Java design pattern deep dive
Observer Pattern in Java: 100 interview questions with professional answers.
Learn how the Observer pattern lets a subject's state changes automatically reach every registered dependent without either side knowing the other's concrete type, why java.util.Observable was deprecated, how Spring's event publisher, reactive streams, and message brokers generalize the same idea, and how to avoid the memory leaks and concurrency bugs that plague real listener code.
What makes a good Observer answer?
Interviewers want to see that you understand a one-to-many dependency, not just "there's a list of listeners": correct decoupling, safe registration lifecycle, and honest handling of failure and ordering during notification.
| Approach | Use when | Watch out for |
|---|---|---|
| Hand-rolled Observer / listener list | A single in-process subject needs to notify a small, known set of in-process observers synchronously. | Manual thread-safety, no built-in backpressure, and easy to forget unregistration. |
| PropertyChangeListener / PropertyChangeSupport | You want a standard JDK bean-property change contract with old/new value pairs and tool support. | String-keyed property names are stringly-typed and typo-prone; still purely in-process. |
| Spring ApplicationEventPublisher / @EventListener | You're already in a Spring container and want decoupled, discoverable listeners with DI, async, and transaction hooks. | Synchronous by default; an unhandled exception in one listener can affect others depending on the multicaster. |
| Reactive streams (Project Reactor / RxJava) | You need backpressure, composition operators, and many subscribers over a stream of values rather than one-shot events. | Steeper learning curve; must reason about subscription lifecycle, schedulers, and hot vs cold sources. |
| Message broker (Kafka, RabbitMQ) as Observer-at-scale | Observers live in different processes or services, need durability, replay, or independent scaling. | Operational overhead, eventual consistency, and no direct method-call semantics or return values. |
Topics
Interview questions and answers
Each answer gives the implementation direction, the trade-off to mention, and the production concern that makes the answer stronger.
1. Explain the Observer design pattern in Java and describe the real-world problem it solves when one object's state change must be reflected in several dependent objects.
The Observer pattern defines a one-to-many dependency between a subject and any number of observers, so that when the subject's state changes, every registered observer is notified and updated automatically, without the subject knowing anything about the observers beyond a shared interface.
It solves the everyday problem of keeping multiple parts of a system in sync with a single source of truth: a spreadsheet cell that must update three charts, a shopping cart total that must refresh a UI badge and a recommendation panel, or a domain object whose state change must trigger email, audit logging, and inventory updates, all without the subject hard-coding calls to each of those concerns.
interface Observer {
void update(String newState);
}
interface Subject {
void addObserver(Observer observer);
void removeObserver(Observer observer);
}
2. What does it mean, precisely, for the Observer pattern to define a "one-to-many dependency" between objects, and how does that differ from a plain method call from one object to several others?
A one-to-many dependency means one subject's state is the single source of truth, and any number of observers depend on that state without the subject knowing how many observers exist, who they are, or what they do with the notification. The dependency is registered dynamically at runtime rather than wired at compile time.
A plain method call from one object to several others, by contrast, hard-codes the exact list of recipients and their concrete types directly into the caller's source code. Adding a new recipient means editing that calling code; with Observer, adding a new observer means only implementing the interface and registering an instance, with zero changes to the subject.
3. Implement a simple Subject/Observer pair from scratch in Java, without relying on the deprecated java.util.Observable, that supports registering, unregistering, and notifying observers of a temperature change.
interface WeatherObserver {
void onTemperatureChanged(double newTempCelsius);
}
class WeatherStation {
private final java.util.List<WeatherObserver> observers = new java.util.ArrayList<>();
private double temperature;
void register(WeatherObserver observer) { observers.add(observer); }
void unregister(WeatherObserver observer) { observers.remove(observer); }
void setTemperature(double newTemp) {
this.temperature = newTemp;
notifyObservers();
}
private void notifyObservers() {
for (WeatherObserver observer : observers) {
observer.onTemperatureChanged(temperature);
}
}
}
class TemperatureDisplay implements WeatherObserver {
@Override
public void onTemperatureChanged(double newTempCelsius) {
System.out.println("Display now shows: " + newTempCelsius + "C");
}
}
This is the pattern every modern Java codebase should hand-roll or reach for a well-supported library equivalent of, rather than extending java.util.Observable, which is deprecated for exactly the reasons covered later in this guide.
4. Name and describe the four classic Gang of Four roles in the Observer pattern: Subject, Observer, ConcreteSubject, and ConcreteObserver, and explain what each is responsible for.
Subject is an interface (or abstract class) declaring the registration methods, typically addObserver/removeObserver, and the responsibility to notify. Observer is an interface declaring a single callback, conventionally named update(), that the subject invokes on state change.
ConcreteSubject holds the actual state of interest and the list of currently registered observers, and calls notifyObservers() whenever that state changes. ConcreteObserver implements the reaction: it stores a reference back to the subject if it needs to pull additional state, and implements update() to do whatever it is that observer cares about, such as refreshing a UI widget or writing an audit record.
5. Why must observers explicitly register and unregister with a subject rather than the subject discovering them automatically, and what API design keeps this lifecycle safe?
Explicit registration exists because the subject has no other way to know an observer exists; Java has no built-in mechanism for a class to "discover" arbitrary listeners at runtime. Registration also gives the observer control over when it starts and stops caring about notifications, which is essential for objects with a shorter lifetime than the subject.
interface Subject<T> {
void addObserver(Observer<T> observer);
boolean removeObserver(Observer<T> observer); // boolean signals "was it even registered?"
}
removeObserver safe to call even if the observer was never registered or was already removed, so cleanup code (for example in a close() or dispose() method) never needs defensive existence checks.6. Introduce the push versus pull models of notification in the Observer pattern: what is the essential difference, and what is a minimal code example of each?
In the push model, the subject sends the full new state (or a rich event object) as an argument to update(), so the observer never needs to call back into the subject. In the pull model, the subject's update() call carries little or no data, often just a reference to itself, and the observer pulls exactly the fields it needs by calling getters on the subject.
// push
interface PushObserver { void update(double newTemperature); }
// pull
interface PullObserver { void update(WeatherStation subject); }
class Display implements PullObserver {
public void update(WeatherStation subject) {
double temp = subject.getTemperature(); // observer decides what it needs
}
}
7. Walk through exactly what happens, line by line, inside a ConcreteSubject's notifyObservers() loop when three different ConcreteObserver implementations are registered.
The subject iterates its internal observer collection and, for each element, invokes the shared update() method polymorphically. Because the collection is typed to the Observer interface, the subject's code is identical regardless of which three concrete classes happen to be registered; the JVM's virtual dispatch resolves each call to the correct concrete implementation at runtime.
private void notifyObservers(OrderEvent event) {
for (OrderObserver observer : new java.util.ArrayList<>(observers)) { // defensive copy, see Q50/Q55
observer.onOrderEvent(event);
}
}
Note the defensive copy of the list before iterating; this protects against a ConcurrentModificationException if any observer registers or unregisters itself from within its own update() callback.
8. What data structure should a ConcreteSubject use internally to hold its list of registered observers, and what trade-offs distinguish an ArrayList, a LinkedHashSet, and a CopyOnWriteArrayList for this purpose?
An ArrayList is the simplest choice for single-threaded or already-externally-synchronized code, and preserves registration order. A LinkedHashSet additionally guarantees no duplicate registrations while still preserving insertion order, useful when accidental double-registration is a realistic bug to guard against.
A CopyOnWriteArrayList is the standard choice once registration and notification can happen concurrently from multiple threads: reads (iteration during notify) never block or throw, at the cost of copying the whole backing array on every add/remove, which is fine because registrations are typically rare compared to notifications.
9. In what sense is the Observer interface's update() method conceptually just a callback, and how does this relate Observer to functional interfaces and lambdas in modern Java?
An Observer interface with a single abstract method is, structurally, a functional interface: it describes "a piece of code to run when something happens," exactly like Runnable or Consumer<T>. This means modern Java code can register a lambda as an observer instead of writing a named class, provided the interface has exactly one abstract method.
@FunctionalInterface
interface OrderObserver { void onOrderPlaced(Order order); }
subject.addObserver(order -> auditLog.record("Order placed: " + order.id()));
subject.addObserver(order -> inventoryService.reserve(order));
removeObserver; keep a named reference if you will need to unregister it.10. Give a plain-English, real-world analogy for the Observer pattern that you would use to explain it to a non-technical stakeholder, and map each part of the analogy back to the pattern's roles.
Think of a magazine subscription: the publisher (subject) maintains a mailing list of subscribers (observers). Subscribers sign up or cancel at will (register/unregister), and the publisher has no idea who its subscribers actually are beyond "someone on the list" — it just mails every issue (notification) to everyone currently subscribed, without changing how it prints or mails based on who's on the list.
The publisher never needs to know a new subscriber exists at compile time; it discovers them purely through the mailing list at runtime, which is exactly how a Subject discovers its Observers only through the shared registration interface.
11. Why were java.util.Observable and java.util.Observer deprecated starting in Java 9, and what specifically is wrong with using them in modern code?
The Javadoc itself states they were deprecated because the mechanism is fundamentally limited: Observable is not serializable-safe, does not support notifications happening from multiple threads with any real thread-safety guarantee, and its design forces observers into a rigid class hierarchy rather than a flexible interface-based one. In short, they never grew to match the needs of a modern concurrent, functional Java application.
extends Observable in a codebase you're reviewing or interviewing about, flag it as legacy code that should be migrated to a custom listener interface, PropertyChangeSupport, or an application framework's own event mechanism.12. Explain why java.util.Observable being a concrete class rather than an interface is itself a design flaw, and how this forced awkward inheritance choices on classes that wanted to be observable.
Because Observable is a class, any class that wanted to be a subject had to extend it, consuming Java's single inheritance slot. A class that already needed to extend something else, for example a Swing component or a domain entity extending a persistence base class, simply could not also extend Observable without restructuring its hierarchy or resorting to composition and manual delegation, which defeats the purpose of using a built-in utility at all.
// impossible: Java has no multiple class inheritance
class TemperatureSensor extends SomeBaseSensor, java.util.Observable { }
A well-designed Subject role should always be an interface, exactly as modern replacements (custom listener interfaces, Spring's ApplicationEventPublisher) are, so it never competes for a class's single inheritance slot.
13. What serialization-safety problems does java.util.Observable have, and why does this matter for objects that need to be persisted or sent across a network?
Observable holds a live list of observer references internally, but it does not implement Serializable in a way that handles that list sensibly; if a subclass is serialized, either the observer list is silently lost, or worse, serialization attempts to drag in whatever arbitrary graph of objects the registered observers reference, which the subject's author never intended to be part of its persisted state.
Modern replacements sidestep this entirely by keeping notification concerns (listener lists) separate from the state that actually needs persisting, and by explicitly marking listener fields transient when a subject class does need to implement Serializable.
14. Describe the thread-safety issues inherent in java.util.Observable's implementation, particularly around its internal "changed" flag and its notifyObservers() method.
Observable uses an internal boolean "changed" flag, set via setChanged() and cleared via clearChanged(), to decide whether notifyObservers() actually notifies anyone. That flag is a single mutable field with no atomic compare-and-set semantics exposed to subclasses, so under concurrent calls from multiple threads, one thread's setChanged() can race with another thread's notifyObservers() clearing it, causing either lost notifications or notifications firing when no real change occurred.
Its observer list is guarded by a single internal lock for all operations, meaning registration, unregistration, and notification all serialize on that lock even when finer-grained or lock-free structures like CopyOnWriteArrayList would perform far better under contention.
15. What replaced java.util.Observable/Observer in modern Java code, and how would you advise a team migrating a legacy codebase away from it?
Modern code generally reaches for one of: a hand-rolled custom listener interface (the most common and most portable choice), java.beans.PropertyChangeSupport for bean-style property notifications, a framework's own eventing mechanism such as Spring's ApplicationEventPublisher, or reactive streams (Project Reactor, RxJava) when backpressure and composition operators are needed.
To migrate a legacy codebase, replace extends Observable with an internal list of a new, purpose-built listener interface, replace addObserver/notifyObservers calls with equivalent methods on that list, and update every implements java.util.Observer class to implement the new interface's differently-named callback method instead — a mechanical but wide-reaching change best done with an IDE's structural search and replace.
16. Demonstrate how to implement the Observer pattern using the JDK's built-in java.beans.PropertyChangeListener and PropertyChangeSupport classes.
class Account {
private final java.beans.PropertyChangeSupport support = new java.beans.PropertyChangeSupport(this);
private java.math.BigDecimal balance = java.math.BigDecimal.ZERO;
void addPropertyChangeListener(java.beans.PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
void deposit(java.math.BigDecimal amount) {
java.math.BigDecimal oldBalance = balance;
balance = balance.add(amount);
support.firePropertyChange("balance", oldBalance, balance); // old/new value pair, built in
}
}
PropertyChangeSupport is thread-safe, already handles the observer list internally, and gives listeners both the old and new value in one call, which a hand-rolled interface would otherwise need to define explicitly.
17. What is a "bound property" in JavaBeans terminology, and how does firePropertyChange let external code react whenever such a property changes without polling?
A bound property is a bean property that notifies registered PropertyChangeListeners whenever its value changes, by convention through a setter that calls support.firePropertyChange(propertyName, oldValue, newValue). Listeners register once and are pushed every subsequent change, eliminating the need to poll the bean's getter repeatedly to detect updates.
account.addPropertyChangeListener(evt -> {
if ("balance".equals(evt.getPropertyName())) {
System.out.println("Balance changed from " + evt.getOldValue() + " to " + evt.getNewValue());
}
});
18. Explain how AWT/Swing's ActionListener and MouseListener interfaces are a ubiquitous, everyday application of the Observer pattern, even though they aren't usually described that way in GUI documentation.
A Swing JButton is the subject: its state of interest is "was I clicked," and any number of ActionListener observers can register via addActionListener. When the button is clicked, it iterates its internal listener list and calls actionPerformed on each, exactly matching the notify-loop shape of a textbook Observer implementation.
JButton button = new JButton("Submit");
button.addActionListener(e -> System.out.println("Clicked!")); // registering an observer
button.addActionListener(e -> auditLog.record("submit-click")); // a second, independent observer
19. What special concurrency rule applies to Swing listeners regarding the Event Dispatch Thread (EDT), and why does violating it lead to intermittent, hard-to-reproduce bugs?
Swing is single-threaded by convention: all UI state reads, writes, and listener callbacks must happen on the Event Dispatch Thread. If a background worker thread calls a listener's update() or directly mutates a Swing component from outside the EDT, you get race conditions and rendering corruption that only manifest occasionally, since the two threads' timing determines whether a visible glitch actually occurs.
// wrong: notifying/mutating Swing from a background thread
new Thread(() -> label.setText("Done")).start();
// correct: marshal back onto the EDT
javax.swing.SwingUtilities.invokeLater(() -> label.setText("Done"));
20. What is the difference between PropertyChangeListener and VetoableChangeListener, and how does the latter let an observer reject a proposed state change?
PropertyChangeListener is purely informational: by the time it fires, the change has already happened and the listener can only react. VetoableChangeListener fires before the change is committed and can throw a checked PropertyVetoException to reject it, in which case the bean must roll back to the old value and re-notify any listeners that already saw the (now-reverted) change.
class Account {
private final java.beans.VetoableChangeSupport vetoSupport = new java.beans.VetoableChangeSupport(this);
void setBalance(java.math.BigDecimal newBalance) throws java.beans.PropertyVetoException {
java.math.BigDecimal old = this.balance;
vetoSupport.fireVetoableChange("balance", old, newBalance); // any listener can veto here
this.balance = newBalance;
}
}
21. Why does a bean's PropertyChangeEvent always include a reference to the event source object, and what practical purpose does this serve when one listener is registered with multiple subjects?
If a single listener instance is registered with several different bean instances (say, three Account objects all sharing one audit listener), the event source lets that one listener's callback determine which specific bean fired the event, since the callback method itself receives no other way to distinguish the caller.
listener = evt -> {
Account source = (Account) evt.getSource();
System.out.println("Account " + source.getId() + " changed " + evt.getPropertyName());
};
22. What principles should guide the design of a custom Observer/listener interface's method signature: should it take individual parameters, an event object, or the subject itself?
Prefer a single, immutable event object over a growing list of individual parameters: it lets you add fields later (a new field on the event class) without breaking every implementation's method signature, and it can carry a timestamp, correlation ID, or the full previous/new state pair cleanly.
record OrderPlacedEvent(String orderId, java.math.BigDecimal total, java.time.Instant occurredAt) {}
interface OrderPlacedListener {
void onOrderPlaced(OrderPlacedEvent event);
}
23. Explain Spring's ApplicationEventPublisher and ApplicationEvent mechanism as an implementation of the Observer pattern within the Spring container.
Spring's ApplicationContext itself acts as the subject: any bean can inject ApplicationEventPublisher and call publishEvent(event), and any other bean can register as an observer simply by declaring a method annotated @EventListener that accepts that event type. Spring's internal ApplicationEventMulticaster maintains the registration and does the notify-loop dispatch for you.
@Service
class OrderService {
private final ApplicationEventPublisher publisher;
OrderService(ApplicationEventPublisher publisher) { this.publisher = publisher; }
void placeOrder(Order order) {
// ... persist the order ...
publisher.publishEvent(new OrderPlacedEvent(order.id(), order.total()));
}
}
24. Show how the @EventListener annotation lets a Spring bean register as an observer without implementing any interface, and explain how Spring determines which events a method should receive.
@Component
class InventoryListener {
@EventListener
public void onOrderPlaced(OrderPlacedEvent event) {
inventoryService.reserveStockFor(event.orderId());
}
}
Spring inspects the annotated method's single parameter type at startup and registers it to receive only events assignable to that type, using reflection rather than requiring the bean to implement a marker interface like the older ApplicationListener<E> required — a much less invasive way to opt in as an observer.
25. Walk through a complete worked example of publishing an OrderPlacedEvent domain event from an order service and having three fully decoupled listeners react to it: sending a confirmation email, updating inventory, and writing an audit log entry.
record OrderPlacedEvent(String orderId, String customerEmail, java.math.BigDecimal total) {}
@Service
class OrderService {
private final ApplicationEventPublisher publisher;
OrderService(ApplicationEventPublisher publisher) { this.publisher = publisher; }
void placeOrder(Order order) {
orderRepository.save(order);
publisher.publishEvent(new OrderPlacedEvent(order.id(), order.customerEmail(), order.total()));
}
}
@Component class EmailListener {
@EventListener void onOrderPlaced(OrderPlacedEvent e) { mailer.sendConfirmation(e.customerEmail(), e.orderId()); }
}
@Component class InventoryListener {
@EventListener void onOrderPlaced(OrderPlacedEvent e) { inventoryService.reserveStockFor(e.orderId()); }
}
@Component class AuditListener {
@EventListener void onOrderPlaced(OrderPlacedEvent e) { auditLog.record("ORDER_PLACED", e.orderId(), e.total()); }
}
None of the three listeners know about each other, and OrderService knows about none of them; adding a fourth reaction, such as a loyalty-points listener, requires zero changes to OrderService or the other listeners.
26. How would you make one of the OrderPlacedEvent listeners run asynchronously using Spring's @Async annotation, and what must be configured for @Async event listeners to actually run off the calling thread?
@Configuration
@EnableAsync
class AsyncConfig {}
@Component
class EmailListener {
@Async
@EventListener
void onOrderPlaced(OrderPlacedEvent event) {
mailer.sendConfirmation(event.customerEmail(), event.orderId()); // runs on a separate thread pool
}
}
@EnableAsync must be present on a configuration class for Spring to proxy @Async-annotated methods at all; without it, @Async is silently ignored and the listener still runs synchronously on the publishing thread.
@Async @EventListener method must return void (or occasionally Future); its result cannot influence whether the publishing code's transaction commits, since it runs on a different thread.27. Explain Spring's @TransactionalEventListener and why it exists: what problem does it solve compared to a plain @EventListener when the listener needs to see committed database state?
A plain @EventListener fires synchronously the instant publishEvent is called, which may be before the surrounding transaction actually commits. If that listener then queries the database for the just-placed order, it can see stale or absent data if the transaction hasn't committed yet, or the transaction could still roll back after the listener already took an irreversible action like sending an email.
@Component
class EmailListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderPlaced(OrderPlacedEvent event) {
mailer.sendConfirmation(event.customerEmail(), event.orderId()); // only fires if the transaction actually committed
}
}
AFTER_COMMIT is the default and most common phase, guaranteeing the listener only runs once the enclosing transaction has successfully committed; other phases like AFTER_ROLLBACK exist for compensating actions.
28. If two @EventListener methods react to the same event and must run in a specific order, how would you control that ordering, and why should you generally avoid relying on it?
@Component
class InventoryListener {
@Order(1)
@EventListener
void reserveStock(OrderPlacedEvent event) { /* runs first */ }
}
@Component
class NotifyListener {
@Order(2)
@EventListener
void notifyWarehouse(OrderPlacedEvent event) { /* runs second, assuming stock is now reserved */ }
}
@Order lets you specify a relative priority, with lower values running first. Relying on this is fragile in general because it couples two supposedly independent listeners together implicitly; if listener B truly depends on listener A having run first, that dependency is better modeled explicitly (A calls B, or a single listener does both steps) rather than through ordering annotations alone.
29. How does Spring resolve generic event types, such as an event class parameterized like PayloadEvent<Order>, when matching @EventListener methods, and what limitation does Java's type erasure impose here?
Because of type erasure, a raw PayloadEvent object at runtime carries no record of its generic parameter, so Spring cannot simply check instanceof PayloadEvent<Order>. Instead, Spring uses ResolvableType, which inspects the actual generic type information captured when the event class was constructed or declared (via ResolvableTypeProvider or reflection on the publishing site), to match listeners against the correct parameterized event type.
class PayloadEvent<T> implements ResolvableTypeProvider {
private final T payload;
PayloadEvent(T payload) { this.payload = payload; }
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(getClass(), payload.getClass());
}
}
30. How would you write a test that verifies publishing an OrderPlacedEvent through Spring's ApplicationEventPublisher actually triggers the InventoryListener with the correct order ID?
@SpringBootTest
class OrderServiceEventTest {
@Autowired ApplicationEventPublisher publisher;
@MockBean InventoryService inventoryService;
@Test
void publishingOrderPlacedTriggersInventoryReservation() {
publisher.publishEvent(new OrderPlacedEvent("order-42", "a@b.com", java.math.BigDecimal.TEN));
verify(inventoryService).reserveStockFor("order-42");
}
}
For a narrower unit test that avoids booting the whole Spring context, instantiate the listener class directly and call its @EventListener-annotated method as a plain method, bypassing Spring's event machinery entirely and testing only the listener's own logic.
31. What is Spring's ApplicationEventMulticaster, and how does it decide whether listeners run synchronously on the publishing thread or asynchronously?
ApplicationEventMulticaster is the internal component that holds the registered listener list and performs the actual notify loop whenever publishEvent is called; it is Spring's concrete realization of the Subject role. The default SimpleApplicationEventMulticaster invokes listeners synchronously on the calling thread unless you configure it with a TaskExecutor, in which case every listener (not just @Async-annotated ones) is dispatched through that executor.
@Bean
ApplicationEventMulticaster applicationEventMulticaster() {
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
multicaster.setTaskExecutor(java.util.concurrent.Executors.newFixedThreadPool(4));
return multicaster;
}
32. What best practices should guide designing a custom domain event class for use with Spring's event mechanism, regarding immutability, naming, and what data it should carry?
Make the event class immutable (a Java record is ideal) so listeners cannot accidentally mutate shared state that other listeners will also observe. Name it in the past tense, describing something that already happened (OrderPlacedEvent, not PlaceOrderEvent), since by the time listeners see it the action is done and cannot be vetoed.
record OrderPlacedEvent(String orderId, java.time.Instant occurredAt, java.math.BigDecimal total) {}
Include enough data for listeners to act without querying back into the publishing service for basic fields, but avoid embedding entire mutable entity graphs; prefer IDs and primitive/value-object fields, letting listeners re-fetch full entities themselves if they need more than the event provides.
33. If one of several synchronous @EventListener methods throws an exception, what happens to the other listeners and to the code that called publishEvent()?
With the default synchronous multicaster, an exception thrown by one listener propagates straight up through publishEvent() to the caller, and any listeners that had not yet been invoked in that notify loop are skipped entirely. This is a meaningful risk: one buggy listener can silently prevent other, unrelated listeners from ever running.
@EventListener
void onOrderPlaced(OrderPlacedEvent event) {
try {
riskyThirdPartyCall(event);
} catch (Exception ex) {
log.error("Listener failed for order {}", event.orderId(), ex); // swallow so siblings still run
}
}
34. How would you bridge an in-process Spring ApplicationEvent to a Kafka topic, so that observers in other microservices can react to the same domain event that in-process listeners react to?
@Component
class KafkaBridgeListener {
private final KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate;
KafkaBridgeListener(KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate) { this.kafkaTemplate = kafkaTemplate; }
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void publishToKafka(OrderPlacedEvent event) {
kafkaTemplate.send("order-events", event.orderId(), event);
}
}
This one bridge listener is itself just another in-process Observer of the domain event; its job is solely to re-publish onto Kafka after the transaction commits, extending the same one-to-many notification out to observers running in entirely separate services and processes.
35. Describe a bug scenario where a synchronous @EventListener performing a slow external call caused the publishing transaction, and the whole HTTP request, to hang or time out.
A synchronous listener runs on the same thread, inside the same transaction, as the code that called publishEvent. If that listener calls a slow third-party API (say, a payment provider webhook confirmation taking several seconds), the entire request thread blocks until it returns, and if the database transaction is still open, the transaction's connection is held open for that whole duration too, exhausting the connection pool under load.
@Async or @TransactionalEventListener(phase = AFTER_COMMIT) combined with @Async, so the original request thread and transaction are released promptly.36. Explain how reactive streams, as implemented by Project Reactor's Flux/Mono or RxJava's Observable, generalize the classic Observer pattern, and what fundamentally new concept they add.
A reactive Publisher is a subject and a Subscriber is an observer, in the same spirit as GoF Observer: a source of values notifies interested parties as those values arrive. The fundamentally new concept reactive streams add is backpressure: a subscriber can tell the publisher how many items it is ready to receive, so a fast producer cannot overwhelm a slow consumer, something the plain Observer pattern has no built-in answer for.
Flux<Integer> source = Flux.range(1, 1000);
source.subscribe(
value -> System.out.println("Received: " + value), // onNext, like update()
error -> System.err.println("Error: " + error), // onError
() -> System.out.println("Done") // onComplete
);
37. Explain the relationship between RxJava's io.reactivex.Observable class and the classic GoF Observer pattern, and what additional lifecycle events RxJava's Observer interface supports beyond a plain update().
RxJava's Observable is intentionally named after the pattern; its subscribers implement an Observer interface with onNext, onError, and onComplete, versus the single generic update() of a textbook Observer. This split lets a stream explicitly signal both successful completion and terminal failure, states the plain Observer pattern leaves entirely up to whatever ad hoc convention the interface designer invents.
io.reactivex.rxjava3.core.Observable.just("a", "b", "c")
.subscribe(
value -> System.out.println("onNext: " + value),
error -> System.err.println("onError: " + error),
() -> System.out.println("onComplete"));
38. What is backpressure in reactive streams, and how does it address a real weakness of the classic Observer pattern when a fast subject notifies a slow observer?
In a plain Observer implementation, notifyObservers() calls every observer's update() synchronously and unconditionally; if the subject produces events faster than an observer can process them, there is no mechanism to slow the subject down or buffer safely, so either events pile up unbounded in memory or are silently dropped.
Flux.interval(java.time.Duration.ofMillis(1))
.onBackpressureDrop(dropped -> log.warn("Dropped: {}", dropped)) // subscriber controls overflow strategy
.subscribe(this::processSlowly);
Backpressure formalizes a request-based protocol: the subscriber calls request(n) to say "send me up to n more items," and the publisher must respect that, giving the consumer explicit control over the rate of notification instead of being at the mercy of however fast the subject fires.
39. Explain the difference between a cold and a hot reactive publisher, and how this distinction maps onto whether a "subject" in the Observer sense already has state before an observer subscribes.
A cold publisher (like Flux.range(1, 5)) starts producing its sequence fresh for each new subscriber, so each subscriber gets the exact same full sequence independently — there is no shared "subject state" being observed, just a repeatable recipe re-run per subscriber. A hot publisher (like a Sinks.many() multicast sink, or a live sensor feed) has ongoing state and emits to whoever happens to be subscribed at that moment, exactly matching the classic Observer subject: subscribers who join late miss earlier notifications entirely.
40. Explain how a Kafka topic implements a distributed, durable version of the Observer pattern across process and service boundaries, and what durability guarantee it adds that in-process Observer lacks.
A Kafka topic is the subject: producers publish events to it, and any number of consumer groups (observers) subscribe independently, each maintaining its own read offset. Because Kafka persists published messages to disk for a configured retention period, a consumer that was offline when an event was published can still read it later by replaying from its last committed offset — durability that a plain in-memory Observer, where a missed notification is gone forever, simply cannot provide.
@KafkaListener(topics = "order-events", groupId = "email-service")
void onOrderEvent(OrderPlacedEvent event) {
mailer.sendConfirmation(event.customerEmail(), event.orderId());
}
41. How does a RabbitMQ fanout or topic exchange implement publish-subscribe, and how does this compare structurally to the Observer pattern's notify-all-registered-observers behavior?
A RabbitMQ fanout exchange broadcasts every message it receives to all queues currently bound to it, mirroring a subject's notify loop calling every registered observer, except the "observers" here are independent queues, each consumed by a possibly different, unrelated service. A topic exchange refines this by routing based on a message's routing key pattern, letting each bound queue effectively subscribe only to a subset of event types, similar to filtering observers by event type in-process (see Q89).
42. What durability and delivery guarantees does a message broker add over an in-memory Observer implementation, and what new failure modes does it introduce in exchange?
A broker adds persistence (messages survive a consumer or even the broker itself restarting), at-least-once or exactly-once delivery semantics, and independent consumer scaling — none of which a plain in-process observer list provides, since an in-process notification that fails is simply lost when the JVM exits.
In exchange, you now must handle duplicate delivery (idempotent observers, see Q74), out-of-order delivery across partitions, consumer lag under load, and the operational overhead of running and monitoring the broker itself — none of which exist when notification is just a synchronous in-memory method call.
43. Precisely distinguish the classic Observer pattern from the publish-subscribe (pub-sub) messaging pattern: what changes structurally when you move from direct object references to a decoupled broker?
In the classic Observer pattern, the subject holds direct in-memory references to its observers and calls their methods synchronously; subject and observers must live in the same process and, typically, the same class loader. In pub-sub, the publisher has no reference to any subscriber at all — it only knows about a topic or channel on a broker, and the broker itself is responsible for routing messages to whichever subscribers currently exist, who may be in entirely different processes, languages, or machines.
// Observer: direct reference, synchronous
subject.addObserver(observer);
subject.notifyObservers();
// Pub-sub: no direct reference, broker-mediated, can be asynchronous and cross-process
kafkaTemplate.send("order-events", event);
// somewhere else, in another service entirely:
@KafkaListener(topics = "order-events") void handle(OrderPlacedEvent e) { ... }
44. What is a Project Reactor Sinks.Many, and how does it act as a programmatic equivalent of a Subject that you can imperatively push values into, bridging non-reactive code into a reactive stream?
Sinks.Many<OrderPlacedEvent> sink = Sinks.many().multicast().onBackpressureBuffer();
// imperative code pushes into the sink, exactly like calling notifyObservers()
void placeOrder(Order order) {
orderRepository.save(order);
sink.tryEmitNext(new OrderPlacedEvent(order.id(), order.total()));
}
Flux<OrderPlacedEvent> events = sink.asFlux(); // any number of subscribers can observe this
events.subscribe(event -> inventoryService.reserveStockFor(event.orderId()));
A Sinks.Many is exactly the reactive-world analog of a hand-rolled Subject: it holds the "current broadcast point," accepts pushes from imperative code, and multicasts to however many Flux subscribers have joined, complete with configurable overflow strategies for slow subscribers.
45. Name the four interfaces defined by the Reactive Streams specification (Publisher, Subscriber, Subscription, Processor) and explain how they map onto the Observer pattern's Subject and Observer roles.
Publisher<T> maps to Subject: it produces a stream of items and accepts subscriptions. Subscriber<T> maps to Observer: it receives onSubscribe, onNext, onError, and onComplete callbacks. Subscription is new: it's the object a subscriber uses to call request(n) for backpressure and cancel() to unregister, which a plain Observer's removeObserver call handles more crudely.
interface Publisher<T> { void subscribe(Subscriber<? super T> s); }
interface Subscriber<T> {
void onSubscribe(Subscription s);
void onNext(T t);
void onError(Throwable t);
void onComplete();
}
Processor<T,R> is both a Subscriber<T> and a Publisher<R> simultaneously, letting it sit in the middle of a chain, transforming and re-emitting.
46. Describe the classic memory leak scenario caused by observers that register with a subject but are never unregistered, and why this is one of the most common real-world bugs in Observer-based code.
Because the subject holds a strong reference to every registered observer in its internal list, an observer that is otherwise unreachable from the rest of the application remains alive for as long as the subject itself lives, simply because the subject's list still points to it. If the subject is long-lived (a singleton event bus, a cache manager, an application-scoped service) and observers are created and discarded far more often than they are unregistered, each forgotten observer is a small permanent leak that accumulates over the application's lifetime.
47. Walk through a concrete GUI example where a short-lived dialog registers as a listener on a long-lived application-scoped component and is never unregistered when the dialog closes, leaking memory.
class SettingsDialog extends JDialog {
SettingsDialog(AppSettings settings) {
settings.addListener(this::onSettingsChanged); // registers, but nothing ever calls removeListener
}
private void onSettingsChanged(Settings newSettings) { /* refresh dialog fields */ }
}
Every time a user opens and closes this dialog, a new listener is added to the application-scoped AppSettings subject, and the closed dialog (along with everything it references) is kept alive by that registration long after the window itself has disappeared from the screen, silently growing heap usage across a long-running session.
48. How can WeakReference or WeakHashMap be used to mitigate observer memory leaks, and what trade-off does this introduce regarding when observers actually get cleaned up?
class WeakObserverList<T> {
private final java.util.List<java.lang.ref.WeakReference<T>> observers = new java.util.ArrayList<>();
void add(T observer) { observers.add(new java.lang.ref.WeakReference<>(observer)); }
void notifyAll(java.util.function.Consumer<T> action) {
observers.removeIf(ref -> ref.get() == null); // prune garbage-collected observers
for (java.lang.ref.WeakReference<T> ref : observers) {
T observer = ref.get();
if (observer != null) action.accept(observer);
}
}
}
Holding observers via WeakReference lets the garbage collector reclaim an observer the rest of the application no longer holds a strong reference to, even though it's still technically registered. The trade-off is that cleanup timing becomes non-deterministic, tied to GC behavior rather than an explicit unregister call, and a forgotten strong reference held elsewhere still defeats the mitigation entirely.
49. Why is CopyOnWriteArrayList commonly recommended for a subject's internal observer list in concurrent code, and what performance trade-off does it make to achieve that safety?
private final java.util.List<Observer> observers = new java.util.concurrent.CopyOnWriteArrayList<>();
CopyOnWriteArrayList guarantees that iterating the list (as the notify loop does) never throws ConcurrentModificationException and never observes a torn or partially-updated state, because every mutation (add/remove) creates an entirely new backing array rather than modifying the existing one in place; any in-progress iteration keeps working against its own snapshot array.
The trade-off is that every registration or unregistration is O(n), copying the whole array, which is a poor fit for a list that changes frequently, but an excellent fit for the very common Observer case where notifications vastly outnumber registrations.
50. Explain exactly why calling removeObserver() from inside another observer's update() method, while the subject is mid-iteration over a plain ArrayList, throws a ConcurrentModificationException.
for (Observer o : observers) { // observers is a plain ArrayList
o.update(event); // if this observer calls subject.removeObserver(anotherObserver) internally...
}
// ...the iterator's internal modCount check fails on the next call to next(), throwing CME
ArrayList's iterator tracks a modCount captured when the iterator was created; any structural modification to the list (including from a nested call triggered by the very iteration you're inside) increments that counter, and the iterator's next next() or hasNext() call detects the mismatch and throws ConcurrentModificationException as a fail-fast safety measure, even though the actual thread involved is the same single thread the whole time.
51. Should calling code ever rely on the order in which multiple observers are notified? Explain why this is generally an unsafe assumption and how to design around it.
No — unless a specific API explicitly documents an ordering guarantee (such as Spring's @Order-annotated listeners, or a subject that deliberately iterates a sorted structure), notification order should be treated as an implementation detail that can change across refreshes, library upgrades, or even between JVM runs if the underlying collection type changes.
Design around it by making each observer's behavior independent of what any other observer has or hasn't done yet; if a true ordering dependency exists (observer B genuinely needs observer A to have run first), model that as an explicit call from A to B, or as a documented ordering contract, rather than hoping registration order happens to be preserved.
52. If one observer's update() method throws an unchecked exception during the subject's notify loop, what happens to the observers that haven't been notified yet, and how would you fix this?
By default, an uncaught exception from one observer's update() propagates straight out of the loop, meaning every observer that would have been notified after the failing one in iteration order simply never gets called, and the caller of notifyObservers() sees an unexpected exception it may not know how to handle.
void notifyObservers(Event event) {
for (Observer observer : observers) {
try {
observer.update(event);
} catch (RuntimeException ex) {
log.error("Observer {} failed to process {}", observer, event, ex); // isolate failure, keep looping
}
}
}
53. Design a robust notification loop that isolates each observer's failure from the others and additionally collects all thrown exceptions to report as a single aggregate failure to the caller.
void notifyObservers(Event event) {
java.util.List<Exception> failures = new java.util.ArrayList<>();
for (Observer observer : observers) {
try {
observer.update(event);
} catch (Exception ex) {
failures.add(ex);
}
}
if (!failures.isEmpty()) {
RuntimeException aggregate = new RuntimeException(failures.size() + " observer(s) failed");
failures.forEach(aggregate::addSuppressed);
throw aggregate; // all observers ran; caller still learns something went wrong
}
}
Using addSuppressed preserves every individual failure's stack trace for diagnostics while guaranteeing every registered observer got a chance to run regardless of whether an earlier one failed.
54. Describe a deadlock or livelock risk that arises when an observer's update() method synchronously calls back into the subject, which is itself holding a lock during the notify loop.
synchronized void notifyObservers(Event event) { // holds the subject's monitor
for (Observer observer : observers) {
observer.update(event); // if this calls subject.someOtherSynchronizedMethod(), same thread reenters fine...
// but if it triggers ANOTHER thread to call a synchronized subject method, that thread blocks
}
}
Because Java monitors are reentrant, the same thread calling back into another synchronized method on the same subject does not deadlock itself; the real danger is a different thread, perhaps woken by the observer's action, trying to acquire that same lock while the notify loop is still in progress, causing it to block until the entire (possibly slow) notification finishes — and if that blocked thread is one the notify loop is itself waiting on, you get a genuine deadlock.
55. What is the safest pattern for allowing an observer to unregister itself from within its own update() callback, without causing a ConcurrentModificationException or skipping other observers?
void notifyObservers(Event event) {
for (Observer observer : java.util.List.copyOf(observers)) { // iterate a defensive snapshot
observer.update(event); // safe even if this call removes itself or others from `observers`
}
}
Iterating a snapshot copy (or using CopyOnWriteArrayList, whose iterator is inherently a fixed snapshot) means a mid-loop removeObserver call mutates the live list safely without affecting the iteration already in progress; the removed observer simply won't be notified again on the next round of notifications.
56. Explain the difference between the Observer pattern and the Mediator pattern, given both involve one object coordinating communication among several others.
Observer models a one-way, one-to-many broadcast: a single subject's state change fans out to many independent observers, none of which talk back to each other through the subject. Mediator models many-to-many communication: several colleague objects all talk through a central mediator, which can route, translate, or sequence messages between any pair of colleagues, not just push out from one to many.
A subject in Observer typically doesn't care what observers do with a notification and never coordinates their interactions; a Mediator actively coordinates and often contains real logic dictating who talks to whom and in what order, closer to an orchestrator than a broadcaster.
57. Explain the difference between the Observer pattern and the Chain of Responsibility pattern, both of which involve passing a request-like object through multiple handlers.
Observer broadcasts to every registered observer unconditionally: all of them receive the notification, and none of them can stop it from reaching the others. Chain of Responsibility passes a request along a chain until exactly one handler (or none) decides to handle it and stops the chain, so downstream handlers may never even see the request.
// Observer: every observer runs
for (Observer o : observers) { o.update(event); }
// Chain of Responsibility: stops at the first handler that handles it
for (Handler h : chain) {
if (h.handle(request)) break; // request stops propagating
}
58. Explain the difference between the Observer pattern and the Strategy pattern, since both involve an object holding a reference to an interface implemented elsewhere.
Strategy holds exactly one interchangeable algorithm implementation at a time, chosen deliberately to vary how a single operation is performed; the context calls the strategy to get a required result back. Observer holds any number of independent listeners, all notified about the same event, typically returning nothing and each reacting in its own unrelated way; the subject doesn't depend on any observer's outcome to proceed.
59. Explain the difference between the Observer pattern and the Command pattern, and describe how they are often used together in undo/redo and event-driven systems.
Command encapsulates a single request as an object so it can be queued, logged, or undone; Observer defines a broadcast relationship so many parties learn about something that already happened. They combine naturally: executing a Command can itself publish an event that multiple observers react to, for example an ExecuteCommand action publishing a CommandExecutedEvent that both an undo-history observer and an audit-log observer independently record.
60. Explain the difference between the Observer pattern and the Visitor pattern, given both involve double-dispatch-like callback method invocations.
Visitor lets you add new operations over a fixed, closed set of element types without modifying those types, by having each element accept a visitor and call back the visitor's type-specific method (true double dispatch). Observer has nothing to do with traversing a type hierarchy of elements; it's about one subject's state change reaching an open, dynamically-registered set of observers, all invoked through the same single update() method regardless of the observer's concrete type.
61. In the classic Model-View-Controller architecture, how do views typically act as observers of the model, and what does this buy the application architecturally?
The model is the subject: it holds the application's real state and knows nothing about which views are displaying it. Each view registers itself as an observer of the model and re-renders whenever it's notified of a change, while the controller updates the model in response to user input. This means the same model can drive multiple simultaneous views (a table and a chart of the same data, for instance) without the model containing any view-specific rendering logic.
62. Some developers confuse the Observer pattern with the Iterator pattern because both involve "going through a sequence of things." Clarify why these patterns solve entirely different problems.
Iterator provides sequential access to elements of an existing, already-assembled aggregate, one at a time, on demand, pulled by the client calling next(). Observer has nothing to do with traversing a collection of data; it's about push-based notification of an event to a dynamically registered set of listeners. The only surface-level similarity is that a subject's notify loop happens to iterate its internal observer list — but that's an implementation detail of Observer, not the Iterator pattern being applied to solve the Observer's problem.
63. Why is a global, application-wide event bus (the Subject) frequently implemented as a Singleton, and what risks does combining Observer with Singleton introduce?
public final class EventBus {
private static final EventBus INSTANCE = new EventBus();
public static EventBus getInstance() { return INSTANCE; }
private final java.util.List<Observer> observers = new java.util.concurrent.CopyOnWriteArrayList<>();
private EventBus() {}
public void register(Observer o) { observers.add(o); }
public void publish(Event e) { observers.forEach(o -> o.update(e)); }
}
A single shared instance is convenient because any class in the application can reach the same bus without it being explicitly passed around, but this convenience carries Singleton's usual risks: hidden global coupling that's hard to trace, difficulty substituting a test double in unit tests, and — specific to Observer — an even higher risk of the memory-leak scenario from Q46/Q47, since a globally reachable subject is the longest-lived subject in the whole application.
64. Describe how the Observer and Command patterns can be combined to build an undo/redo system where every executed command is recorded by independent, decoupled observers.
interface Command { void execute(); void undo(); }
class CommandInvoker {
private final java.util.List<CommandExecutedListener> listeners = new java.util.ArrayList<>();
void addListener(CommandExecutedListener l) { listeners.add(l); }
void execute(Command command) {
command.execute();
listeners.forEach(l -> l.onExecuted(command)); // observers react without the invoker knowing who they are
}
}
class UndoHistoryListener implements CommandExecutedListener {
private final java.util.Deque<Command> history = new java.util.ArrayDeque<>();
public void onExecuted(Command command) { history.push(command); }
}
The invoker only knows it must run a Command and notify listeners afterward; whether a listener maintains undo history, writes an audit trail, or updates a "last action" UI label is entirely up to that listener, decoupled from command execution itself.
65. In a system design interview, how would you decide between a plain in-process Observer/listener list and reaching for a message broker when a feature requires "notify several things when X happens"?
Start from the actual requirements rather than the pattern name: if all interested parties live in the same process, need the notification synchronously or with negligible delay, and the number of observers is small and known, a plain listener list is simpler, faster, and has no operational cost. Reach for a broker once observers live in different services or processes, must survive the publisher restarting, need independent scaling or replay, or the number of subscribers can grow without the publisher's code needing to change.
A strong answer explicitly names this trade-off rather than defaulting to "just use Kafka for everything," since an in-process listener list handles the vast majority of single-service notification needs with far less complexity.
66. How would you unit test that a subject notifies exactly the observers registered with it, with the correct event data, using a mocking framework?
@Test
void notifiesRegisteredObserverWithCorrectEvent() {
OrderObserver observer = mock(OrderObserver.class);
OrderSubject subject = new OrderSubject();
subject.addObserver(observer);
subject.placeOrder(new Order("order-1", java.math.BigDecimal.TEN));
ArgumentCaptor<OrderPlacedEvent> captor = ArgumentCaptor.forClass(OrderPlacedEvent.class);
verify(observer).onOrderPlaced(captor.capture());
assertThat(captor.getValue().orderId()).isEqualTo("order-1");
}
Also test that an unregistered observer is never called (using verifyNoInteractions), and that removing an observer mid-test stops further notifications from reaching it.
67. How would you test an @Async Spring event listener, given that its execution happens on a separate thread and a naive test might assert before the listener has actually run?
@Test
void asyncListenerEventuallyProcessesEvent() {
publisher.publishEvent(new OrderPlacedEvent("order-1", "a@b.com", java.math.BigDecimal.TEN));
org.awaitility.Awaitility.await()
.atMost(java.time.Duration.ofSeconds(2))
.untilAsserted(() -> verify(mailer).sendConfirmation("a@b.com", "order-1"));
}
A library like Awaitility polls the assertion repeatedly until it passes or a timeout elapses, which is the correct way to test asynchronous side effects; a bare, unretried assertion immediately after publishing is a classic flaky-test bug because the async listener may simply not have run yet.
68. Provide concrete Java code contrasting a push-model observer, which receives the full new state, against a pull-model observer, which receives only a signal and must call back into the subject for details.
// Push: subject sends everything the observer could need
interface PushObserver { void onPriceChanged(String ticker, double oldPrice, double newPrice); }
// Pull: subject only signals "something changed"; observer decides what to fetch
interface PullObserver { void onChanged(StockTicker subject); }
class Dashboard implements PullObserver {
public void onChanged(StockTicker subject) {
double price = subject.getPrice(subject.getTickerSymbol()); // observer pulls only what it needs
}
}
Push is simpler for observers that always need the same data and avoids extra calls back into the subject; pull is more flexible when different observers need very different subsets of the subject's state and you don't want to bloat the notification payload with fields most observers will ignore.
69. Describe a common production bug where an observer's update() method synchronously performs expensive work, such as a blocking network call, and explain the effect this has on the subject's entire notify loop.
class SlowAuditObserver implements OrderObserver {
public void onOrderPlaced(OrderPlacedEvent event) {
httpClient.post("https://audit.example.com/log", event); // blocking call, several hundred ms
}
}
Because the notify loop calls every observer's update() synchronously, one slow observer's blocking call delays every observer registered after it in the loop, and delays the subject's own calling code from proceeding until the entire loop finishes — a single misbehaving observer degrades the latency of the whole notification path, even for observers that individually would have been instantaneous.
70. What is the standard fix for a slow, blocking observer degrading the subject's notify loop, and what does the fixed code look like?
class SlowAuditObserver implements OrderObserver {
private final java.util.concurrent.ExecutorService pool = java.util.concurrent.Executors.newFixedThreadPool(2);
public void onOrderPlaced(OrderPlacedEvent event) {
pool.submit(() -> httpClient.post("https://audit.example.com/log", event)); // dispatch and return immediately
}
}
Dispatch the expensive work to a background executor from within the observer's own update(), returning control to the notify loop immediately; the subject and any observers registered after this one are no longer held up. The trade-off is that the observer's error handling must now happen asynchronously too, since exceptions thrown inside the submitted task no longer propagate back to the caller of update().
71. Design a subject that dispatches notifications to all observers concurrently via a shared ExecutorService rather than looping through them sequentially, and discuss the trade-offs.
class ConcurrentNotifyingSubject {
private final java.util.List<Observer> observers = new java.util.concurrent.CopyOnWriteArrayList<>();
private final java.util.concurrent.ExecutorService pool = java.util.concurrent.Executors.newFixedThreadPool(8);
void notifyObservers(Event event) {
java.util.List<java.util.concurrent.CompletableFuture<Void>> futures = observers.stream()
.map(o -> java.util.concurrent.CompletableFuture.runAsync(() -> o.update(event), pool))
.toList();
java.util.concurrent.CompletableFuture.allOf(futures.toArray(new java.util.concurrent.CompletableFuture[0])).join();
}
}
Fanning out concurrently bounds total notification latency to the slowest single observer instead of the sum of all observers' durations, at the cost of needing a properly sized thread pool, losing any implicit ordering guarantee entirely, and requiring careful aggregation of per-observer failures (see Q53) since they now happen on different threads.
72. How would you add a per-observer timeout so that one hanging observer cannot block a notify loop indefinitely, even when notifications are dispatched asynchronously?
java.util.concurrent.CompletableFuture.runAsync(() -> observer.update(event), pool)
.orTimeout(500, java.util.concurrent.TimeUnit.MILLISECONDS)
.exceptionally(ex -> {
log.warn("Observer {} timed out or failed", observer, ex);
return null;
});
orTimeout ensures the future completes exceptionally if the observer hasn't finished within the deadline, letting the caller move on and log the problem rather than waiting forever for a hung or unresponsive observer, which matters most when observers make external network calls with unpredictable latency.
73. Describe how a circuit breaker could be applied around a flaky external observer so that repeated failures don't keep slowing down every notification cycle.
class CircuitBreakingObserver implements Observer {
private final Observer delegate;
private final CircuitBreaker breaker; // e.g. Resilience4j CircuitBreaker
CircuitBreakingObserver(Observer delegate, CircuitBreaker breaker) {
this.delegate = delegate; this.breaker = breaker;
}
public void update(Event event) {
if (breaker.tryAcquirePermission()) {
try { delegate.update(event); breaker.onSuccess(); }
catch (Exception ex) { breaker.onError(ex); }
} // else: circuit open, skip this notification entirely, fail fast
}
}
Wrapping a flaky observer in a circuit breaker (itself effectively a decorator around the Observer interface) means once the observer has failed repeatedly, subsequent notifications skip it immediately rather than paying its full failure latency every single time, until the breaker allows a trial call through again.
74. Why must an observer that reacts to messages delivered through a message broker be designed to be idempotent, and what does a non-idempotent observer bug look like in practice?
Most brokers offer at-least-once delivery, meaning the same message can legitimately be delivered more than once (after a consumer crash and redelivery, for instance). An observer that isn't idempotent, such as one that increments an inventory count every time it sees a message rather than checking whether it already processed that exact event ID, will silently double-count or double-charge on redelivery.
@KafkaListener(topics = "order-events")
void onOrderPlaced(OrderPlacedEvent event) {
if (processedEventIds.contains(event.orderId())) return; // idempotency check
inventoryService.reserveStockFor(event.orderId());
processedEventIds.add(event.orderId());
}
75. What subtle bug can occur in removeObserver() if the Observer interface relies on default equals()/hashCode() identity, and how does this affect lambda-based observer registrations specifically?
If observers are stored in a collection that uses equals() for lookup (or if removeObserver itself calls .equals() to find the matching entry), a lambda expression passed at registration time and a separately-created "equivalent" lambda passed to removeObserver are two distinct objects with reference-identity-based equals(), so the removal silently fails to find and remove anything.
OrderObserver observer = event -> log.info("order: {}", event);
subject.addObserver(observer);
subject.removeObserver(observer); // works: same object reference
subject.addObserver(event -> log.info("order: {}", event));
subject.removeObserver(event -> log.info("order: {}", event)); // fails silently: different lambda instance
76. Design a stock ticker system using the Observer pattern where a StockTicker subject notifies multiple Display observers (a table view and a chart view) whenever a stock's price changes.
interface PriceObserver { void onPriceChanged(String symbol, double price); }
class StockTicker {
private final java.util.List<PriceObserver> observers = new java.util.concurrent.CopyOnWriteArrayList<>();
void subscribe(PriceObserver o) { observers.add(o); }
void updatePrice(String symbol, double newPrice) {
observers.forEach(o -> o.onPriceChanged(symbol, newPrice));
}
}
class TableView implements PriceObserver {
public void onPriceChanged(String symbol, double price) { /* update a row */ }
}
class ChartView implements PriceObserver {
public void onPriceChanged(String symbol, double price) { /* append a data point */ }
}
This is the textbook motivating example for Observer precisely because it makes the decoupling tangible: adding a third display, a mobile push notification, requires zero changes to StockTicker.
77. Explain how a data-binding UI framework's model-view binding, such as a JavaFX property bound to a label's text, is itself an Observer-pattern implementation under the hood.
javafx.beans.property.StringProperty name = new javafx.beans.property.SimpleStringProperty("Alice");
label.textProperty().bind(name); // label becomes an observer of the name property
name.set("Bob"); // label automatically updates, no manual refresh call needed
Under the hood, bind() registers a listener on the source property so that whenever its value changes, the bound property (the label's text) is automatically recomputed and refreshed — the exact same subject-notifies-observer mechanics as a hand-rolled Observer, just wrapped in a fluent binding API so the developer never writes the notify loop themselves.
78. Design a simple chat room server using the Observer pattern where a ChatRoom subject broadcasts every incoming message to all currently connected User observers except the sender.
interface ChatObserver { void onMessage(String sender, String message); }
class ChatRoom {
private final java.util.Map<String, ChatObserver> participants = new java.util.concurrent.ConcurrentHashMap<>();
void join(String username, ChatObserver observer) { participants.put(username, observer); }
void leave(String username) { participants.remove(username); }
void broadcast(String sender, String message) {
participants.forEach((username, observer) -> {
if (!username.equals(sender)) observer.onMessage(sender, message); // exclude the sender
});
}
}
Keying the observer collection by username, rather than a plain list, makes both "exclude the sender" and "look up and remove a specific participant on disconnect" straightforward.
79. Design an online auction system where a Bidder registers as an Observer of an AuctionItem and is notified whenever a new highest bid is placed, including being told whether their own previous bid was just outbid.
interface BidObserver { void onNewHighestBid(String bidderId, java.math.BigDecimal amount); }
class AuctionItem {
private final java.util.List<BidObserver> observers = new java.util.concurrent.CopyOnWriteArrayList<>();
private java.math.BigDecimal highestBid = java.math.BigDecimal.ZERO;
private String highestBidderId;
void placeBid(String bidderId, java.math.BigDecimal amount) {
if (amount.compareTo(highestBid) <= 0) throw new IllegalArgumentException("Bid too low");
highestBid = amount;
highestBidderId = bidderId;
observers.forEach(o -> o.onNewHighestBid(bidderId, amount));
}
}
Each Bidder observer can compare the notified bidderId against its own ID to decide whether it was just outbid and should alert its user, entirely on the observer side, with the subject staying unaware of any individual bidder's own state.
80. Design an IoT temperature sensor system where a single physical sensor (subject) broadcasts new readings to a logging observer, an alerting observer, and a dashboard observer, each with very different reaction logic.
interface SensorObserver { void onReading(double celsius, java.time.Instant timestamp); }
class LoggingObserver implements SensorObserver {
public void onReading(double c, java.time.Instant t) { log.info("{} -> {}C", t, c); }
}
class AlertingObserver implements SensorObserver {
public void onReading(double c, java.time.Instant t) {
if (c > 80.0) alertService.pageOnCall("Overheat: " + c + "C at " + t);
}
}
class DashboardObserver implements SensorObserver {
public void onReading(double c, java.time.Instant t) { dashboardCache.updateLatest(c, t); }
}
The sensor's firmware or driver code that reads the physical value and calls notifyObservers() never changes as new reaction logic (a new alerting threshold, a new dashboard widget) is added — only new observer classes are written and registered.
81. Explain how the Observer pattern can be used to implement cache invalidation, where a data-change subject notifies a cache observer to evict or refresh a stale entry.
interface DataChangeObserver { void onEntityChanged(String entityType, String id); }
class CacheInvalidationObserver implements DataChangeObserver {
private final Cache cache;
public void onEntityChanged(String entityType, String id) {
cache.evict(entityType + ":" + id); // stale entry removed as soon as the source data changes
}
}
Wiring cache invalidation as an observer of the same domain events already published for other purposes (auditing, notifications) means the cache never needs its own bespoke "did anything change" polling logic; it just reacts to the same notifications everything else does.
82. How would you use the Observer pattern to implement configuration hot-reloading, where services automatically pick up new configuration values without restarting?
interface ConfigChangeObserver { void onConfigChanged(Config newConfig); }
class ConfigWatcher {
private final java.util.List<ConfigChangeObserver> observers = new java.util.concurrent.CopyOnWriteArrayList<>();
void subscribe(ConfigChangeObserver o) { observers.add(o); }
void onFileChanged(java.nio.file.Path configPath) {
Config reloaded = Config.parse(configPath);
observers.forEach(o -> o.onConfigChanged(reloaded));
}
}
class ConnectionPoolService implements ConfigChangeObserver {
public void onConfigChanged(Config newConfig) { pool.resize(newConfig.getMaxPoolSize()); }
}
A file-watcher thread (using java.nio.file.WatchService) acts as the trigger that turns a filesystem event into a subject notification, letting any number of services adjust their in-memory settings live.
83. Describe using the Observer pattern to implement a cross-cutting audit logging concern, where any domain action publishes an event that a dedicated AuditObserver records, independent of the action's core business logic.
interface DomainEventObserver { void onDomainEvent(DomainEvent event); }
class AuditObserver implements DomainEventObserver {
public void onDomainEvent(DomainEvent event) {
auditRepository.save(new AuditEntry(event.getClass().getSimpleName(), event.occurredAt(), event.actorId()));
}
}
Because every domain action already publishes its own specific event for its own reasons, a single generic AuditObserver registered against the base DomainEvent type can capture a uniform audit trail across the entire application without each business service writing its own audit-logging call inline.
84. Explain how an editor application's undo/redo history feature can be implemented as an Observer of every edit action performed on a document.
interface EditObserver { void onEdit(EditAction action); }
class UndoHistory implements EditObserver {
private final java.util.Deque<EditAction> undoStack = new java.util.ArrayDeque<>();
public void onEdit(EditAction action) { undoStack.push(action); }
void undoLast() {
if (!undoStack.isEmpty()) undoStack.pop().undo();
}
}
The document's editing code just performs each edit and notifies observers; it does not need to know anything about undo history bookkeeping at all, since that responsibility lives entirely inside the UndoHistory observer.
85. In a microservices architecture, how does a service registry or service discovery mechanism act as a subject, notifying interested clients whenever an instance of a dependent service becomes available or unavailable?
interface ServiceInstanceObserver { void onInstancesChanged(java.util.List<ServiceInstance> current); }
class ServiceRegistry {
private final java.util.List<ServiceInstanceObserver> observers = new java.util.concurrent.CopyOnWriteArrayList<>();
void watch(ServiceInstanceObserver o) { observers.add(o); }
private void onHeartbeatChange(String serviceName, java.util.List<ServiceInstance> instances) {
observers.forEach(o -> o.onInstancesChanged(instances));
}
}
A client-side load balancer registers as an observer of the registry so its routing table stays current the moment instances scale up, scale down, or fail health checks, without ever needing to poll the registry on a fixed interval.
86. Explain how webhooks, where a third-party service POSTs an HTTP request to a URL you configured whenever something happens on their side, are a cross-organization implementation of the Observer pattern.
The third-party service is the subject; registering a webhook URL is the equivalent of calling addObserver, except registration happens once, out of band, through their dashboard or API rather than an in-process method call. Instead of a direct method invocation, "notification" takes the form of an HTTP POST to your configured endpoint whenever their internal event fires, and your endpoint acts as the observer's update() method, deserializing the payload and reacting.
@PostMapping("/webhooks/payment-provider")
ResponseEntity<Void> onPaymentWebhook(@RequestBody PaymentWebhookPayload payload) {
if (payload.status().equals("succeeded")) orderService.markPaid(payload.orderId());
return ResponseEntity.ok().build();
}
87. Explain how domain events in Domain-Driven Design relate to the Observer pattern, and why a domain event is typically raised from within an aggregate rather than from application service code.
A domain event captures "something significant happened within the domain" as an immutable, past-tense record; publishing it and having decoupled listeners react is structurally the Observer pattern applied at the architectural level. DDD convention favors raising the event from inside the aggregate whose invariant actually changed (an Order aggregate raising OrderPlacedEvent from within its own place() method), because that keeps the event's truth tightly coupled to the exact state transition that produced it, rather than trusting whichever application service happens to call the aggregate to also remember to raise the event correctly.
class Order {
private final java.util.List<Object> domainEvents = new java.util.ArrayList<>();
void place() {
this.status = OrderStatus.PLACED;
domainEvents.add(new OrderPlacedEvent(id, total)); // recorded, published after successful save
}
java.util.List<Object> pullDomainEvents() {
java.util.List<Object> events = java.util.List.copyOf(domainEvents);
domainEvents.clear();
return events;
}
}
88. How would you design a generic, typed event bus using Java generics so that observers can subscribe to a specific event class and only receive events of that exact type, with compile-time type safety?
class TypedEventBus {
private final java.util.Map<Class<?>, java.util.List<java.util.function.Consumer<?>>> listeners = new java.util.concurrent.ConcurrentHashMap<>();
<T> void subscribe(Class<T> eventType, java.util.function.Consumer<T> listener) {
listeners.computeIfAbsent(eventType, k -> new java.util.concurrent.CopyOnWriteArrayList<>()).add(listener);
}
@SuppressWarnings("unchecked")
<T> void publish(T event) {
java.util.List<java.util.function.Consumer<?>> forType = listeners.get(event.getClass());
if (forType == null) return;
for (java.util.function.Consumer<?> listener : forType) {
((java.util.function.Consumer<T>) listener).accept(event);
}
}
}
Keying the internal map by Class<T> and requiring callers to pass the matching class token at subscription time gives compile-time type safety at the call sites, even though the unchecked cast inside publish is unavoidable due to type erasure of the map's value lists.
89. Building on a generic typed event bus, how would you let an observer subscribe to only a subset of events matching a predicate, such as only OrderPlacedEvents above a certain total amount?
<T> void subscribeIf(Class<T> eventType, java.util.function.Predicate<T> filter, java.util.function.Consumer<T> listener) {
subscribe(eventType, event -> { if (filter.test(event)) listener.accept(event); });
}
eventBus.subscribeIf(OrderPlacedEvent.class,
e -> e.total().compareTo(new java.math.BigDecimal("1000")) > 0,
e -> fraudReviewService.flagForReview(e.orderId()));
Wrapping the predicate check inside the registered consumer keeps filtering entirely on the observer side; the subject and event bus remain unaware that filtering is even happening, which preserves the same decoupling Observer is meant to provide.
90. How would you implement priority-ordered observers, where some observers must run before others during the same notification, without violating the general rule that ordering shouldn't normally be relied upon?
class PriorityObserverList {
private final java.util.List<PrioritizedObserver> observers = new java.util.concurrent.CopyOnWriteArrayList<>();
void add(int priority, Observer observer) {
observers.add(new PrioritizedObserver(priority, observer));
observers.sort(java.util.Comparator.comparingInt(PrioritizedObserver::priority)); // explicit, documented order
}
}
record PrioritizedObserver(int priority, Observer observer) {}
The key distinction from Q51's warning is that here the ordering is an explicit, documented part of the API contract (a numeric priority parameter callers must supply), not an accidental byproduct of registration order or collection implementation, which is exactly what makes relying on it safe in this specific design.
91. What problems arise when trying to serialize a subject object whose observer list holds references to arbitrary, possibly non-serializable observer instances?
If a subject implements Serializable without excluding its observer list, Java's default serialization walks the entire object graph, including every registered observer and anything each observer itself references. If even one observer (or something it references, such as a database connection or a GUI component) isn't Serializable, serialization fails at runtime with a NotSerializableException, often for a field the developer didn't even think of as "part of the subject's real state."
class Subject implements java.io.Serializable {
private transient java.util.List<Observer> observers = new java.util.ArrayList<>(); // excluded from serialization
}
transient so serialization skips it entirely, and re-populate it (typically empty) after deserialization, since listener registrations are runtime wiring, not persistable state.92. What security implications arise from allowing an untrusted or third-party-supplied observer to register with a subject that holds sensitive data?
Because the subject typically passes its own state (or a reference to itself, in the pull model) into every registered observer's callback, an untrusted observer implementation gets direct access to whatever the subject exposes, with no sandboxing. A malicious or careless plugin registered as an observer could exfiltrate sensitive fields, throw exceptions designed to disrupt other observers, or perform expensive work that degrades the notify loop for everyone (see Q69).
93. Beyond JavaFX property binding, name other reactive UI frameworks or libraries whose data-binding mechanism is fundamentally an Observer pattern implementation, and explain the common thread.
Android's LiveData and Jetpack Compose's State/mutableStateOf, Vaadin's data binder, and even the reactive core underlying frontend frameworks outside the JVM ecosystem, all share the same essential shape: a piece of observable state notifies whoever is currently watching it whenever that state changes, and the watching UI element automatically re-renders in response, with no manual "refresh" call anywhere in application code.
LiveData<Integer> counter = new MutableLiveData<>(0);
counter.observe(lifecycleOwner, value -> textView.setText(String.valueOf(value))); // observer auto-registered/unregistered with the lifecycle
LiveData additionally ties observer registration to a component's lifecycle automatically, directly solving the memory-leak problem from Q46/Q47 by design.
94. Summarize the most common mistakes developers make when implementing the Observer pattern in Java, drawing together the pitfalls covered throughout this guide.
- Forgetting to unregister observers, causing memory leaks (Q46, Q47).
- Relying on notification order without an explicit, documented ordering contract (Q51).
- Letting one observer's exception silently prevent other observers from being notified (Q52).
- Performing slow, blocking work directly inside update(), stalling the whole notify loop (Q69).
- Using a plain, non-thread-safe list for the observer collection under concurrent access (Q49, Q50).
- Passing more subject state into observers than they need, widening the security and coupling surface (Q92).
95. What is "event soup," and how does overusing the Observer pattern for too much of an application's control flow make a system harder to understand and debug?
Event soup describes a codebase where so much behavior is wired through implicit event publish/subscribe relationships that no single place in the code shows the actual sequence of what happens when a user action occurs; understanding a single feature requires grepping for every listener of every event it might trigger, and those listeners can themselves publish further events, cascading in ways that are difficult to trace or reproduce in a debugger.
96. Walk through the concrete steps of migrating a class from extending java.util.Observable to a custom listener-interface-based Observer implementation, without breaking existing callers.
// before
class WeatherStation extends java.util.Observable {
void setTemperature(double t) { setChanged(); notifyObservers(t); }
}
// after
interface TemperatureListener { void onTemperatureChanged(double newTemp); }
class WeatherStation {
private final java.util.List<TemperatureListener> listeners = new java.util.concurrent.CopyOnWriteArrayList<>();
void addListener(TemperatureListener l) { listeners.add(l); }
void removeListener(TemperatureListener l) { listeners.remove(l); }
void setTemperature(double t) { listeners.forEach(l -> l.onTemperatureChanged(t)); }
}
Replace each caller's addObserver(this) with addListener(this::onTemperatureChanged) (or an explicit implementing class), and replace each implements java.util.Observer class's update(Observable o, Object arg) method with the new interface's differently-typed, differently-named callback — a mechanical rename-and-retype migration best done incrementally, one caller at a time, behind a temporary adapter if both old and new call sites must coexist during the transition.
97. What should be documented alongside an Observer/listener interface's Javadoc to prevent the kinds of contract-mismatch bugs described elsewhere in this guide?
Document explicitly: whether notifications are synchronous or asynchronous, on which thread they fire (important for GUI and Spring code, Q19/Q35), whether notification order is guaranteed or unspecified (Q51), what happens if a listener throws (Q52/Q53), whether the same event instance can be delivered more than once (Q74), and whether it is safe to register or unregister from within the callback itself (Q55).
98. How would you version an event payload class over time as new fields are needed, without breaking existing observers that were written against the original shape?
// v1
record OrderPlacedEvent(String orderId, java.math.BigDecimal total) {}
// v2: additive change, existing observers referencing orderId/total keep compiling
record OrderPlacedEvent(String orderId, java.math.BigDecimal total, String currencyCode) {
OrderPlacedEvent(String orderId, java.math.BigDecimal total) { this(orderId, total, "USD"); } // back-compat constructor
}
Prefer additive, backward-compatible changes (new optional fields with sensible defaults) over renaming or removing existing fields; if a breaking change is unavoidable, introduce a new event type (OrderPlacedEventV2) alongside the old one for a transition period rather than mutating the original type's shape out from under existing listeners.
99. Describe the performance risk of a "broadcast storm," where a very high-frequency subject notifies a large number of observers on every single state change, and how you would mitigate it.
If a subject changes state thousands of times per second (a live sensor feed, a fast-moving price ticker) and notifies dozens of observers synchronously on every single change, the total work per state change scales with the number of observers, and CPU time spent purely on notification dispatch can dwarf the time spent on the actual state update itself, especially once observer count and change frequency both grow.
// mitigation: coalesce rapid updates into a periodic snapshot notification
Flux.interval(java.time.Duration.ofMillis(100))
.map(tick -> latestPriceRef.get())
.distinctUntilChanged()
.subscribe(price -> observers.forEach(o -> o.onPriceChanged(price)));
Common mitigations include throttling or debouncing notifications to a maximum rate, coalescing multiple rapid changes into a single "latest state" notification, and moving to a reactive stream with backpressure (Q38) so observers that can't keep up are handled explicitly rather than being flooded.
100. As a capstone system design question: given a requirement that "when a customer places an order, several independent systems must react," walk through how you would decide between a plain in-process Observer, Spring application events, and a Kafka-based pub-sub design, and justify your final choice.
Start by enumerating who actually needs to react and where they live: if every reactor (inventory, email, audit) is a bean inside the same Spring application, Spring's ApplicationEventPublisher with @EventListener/@TransactionalEventListener gives clean in-process decoupling with negligible operational cost, matching the "in-process, synchronous, small number of listeners" branch from this guide's decision diagram.
If some reactors are genuinely separate services (a recommendations service owned by another team, a data warehouse ingestion pipeline), or the event must survive the order service restarting, or subscriber count and throughput are expected to grow independently of the order service's own scaling, a Kafka topic is the better fit, with the Spring event mechanism still used internally and one dedicated @TransactionalEventListener bridging the in-process event onto the topic after commit (see Q34).
A strong answer states this as a spectrum rather than a binary choice, justifies the decision against the specific reactors named in the requirement rather than defaulting to either extreme, and notes that the two approaches compose well together rather than being mutually exclusive.
Post a Comment
Add