Fundamentals 14 min read

Observer Pattern: Decoupling Order Payment Success Actions via Publish‑Subscribe

The article explains the Observer pattern by first showing the problems of hard‑coding post‑payment actions, then detailing how defining a subject, observer interface, and concrete observers decouples the system, discusses push vs pull notification models, and illustrates real‑world uses in Spring events, GUI listeners, and message queues.

Dabaoshi
Dabaoshi
Dabaoshi
Observer Pattern: Decoupling Order Payment Success Actions via Publish‑Subscribe

1. Hard‑coding post‑payment actions

When an order is paid, a naïve implementation puts every downstream action—adding points, sending SMS, notifying logistics, updating sales statistics—directly inside OrderService.paySuccess. This creates:

Tight coupling : OrderService must know every downstream service.

Violation of the Open/Closed Principle : adding a new action requires modifying the method.

Mixed responsibilities : the method handles payment logic and unrelated side‑effects, some of which may be asynchronous or need retry.

public class OrderService {
    public void paySuccess(Order order) {
        // after payment, notify downstream services
        pointService.addPoints(order);      // add points
        smsService.send(order);            // send SMS
        logisticsService.prepare(order);   // notify logistics
        statService.updateSales(order);    // update sales stats
        // tomorrow we may add "send coupon" ...
    }
}

The root cause is that the event producer (payment success) and the event consumers are hard‑coded together.

2. Observer pattern: subscription and notification

Step 1 – Define the observer interface :

// Observer: all downstream actions implement this
public interface OrderObserver {
    void onPaySuccess(Order order);
}

Step 2 – Implement concrete observers :

public class PointObserver implements OrderObserver {
    public void onPaySuccess(Order order) { /* add points */ }
}
public class SmsObserver implements OrderObserver {
    public void onPaySuccess(Order order) { /* send SMS */ }
}
public class LogisticsObserver implements OrderObserver {
    public void onPaySuccess(Order order) { /* notify logistics */ }
}

Step 3 – Subject maintains the observer list and notifies :

public class OrderSubject {
    // observer list
    private final List<OrderObserver> observers = new ArrayList<>();
    public void subscribe(OrderObserver o) { observers.add(o); }
    public void unsubscribe(OrderObserver o) { observers.remove(o); }
    // when payment succeeds, broadcast to all observers
    public void paySuccess(Order order) {
        // ... handle payment itself
        for (OrderObserver o : observers) {
            o.onPaySuccess(order); // notify each observer
        }
    }
}

Usage:

OrderSubject subject = new OrderSubject();
subject.subscribe(new PointObserver());   // points
subject.subscribe(new SmsObserver());      // SMS
subject.subscribe(new LogisticsObserver()); // logistics
subject.paySuccess(order); // one call, all observers react

Compared with the hard‑coded version, OrderSubject no longer knows concrete downstream services; it only depends on the OrderObserver abstraction, satisfying the Open/Closed Principle.

Observer broadcast diagram
Observer broadcast diagram

3. Roles and push vs. pull models

Abstract Subject – e.g. OrderSubject (can be an interface). Maintains the observer list and provides subscribe/unsubscribe methods.

Concrete Subject – the actual implementation that notifies observers when state changes.

Abstract Observer – the OrderObserver interface that defines the event‑handling method.

Concrete Observer – classes such as PointObserver, SmsObserver, LogisticsObserver that implement specific response logic.

The key design choice is how much data the subject passes to observers:

Push model : the subject pushes full data, e.g. void onPaySuccess(Order order). Simple but may transmit unnecessary information.

Pull model : the subject only signals a change, e.g. void onEvent(OrderSubject subject). Observers pull what they need via methods on the subject, offering flexibility at the cost of an extra step.

// Push model
void onPaySuccess(Order order); // receives all data
// Pull model
void onEvent(OrderSubject subject); // observer calls subject.getXxx()

Choose push when observers need essentially the same data; choose pull when data requirements vary or are large.

4. Real‑world appearances

Spring events (ApplicationEvent + ApplicationListener): publishing new OrderPaidEvent(order) notifies all @EventListener methods, optionally async with @Async.

GUI listeners : button click listeners in Swing ( addActionListener) or front‑end addEventListener follow the same pattern.

Message‑queue pub‑sub : Kafka, RabbitMQ topics broadcast to all subscribed consumers.

JDK Observer : java.util.Observer / Observable (deprecated since Java 9).

5. When to use and when not to use

Use when:

An object's state change must automatically notify a dynamically changing set of other objects.

The producer should be decoupled from the consumers.

There is a natural one‑to‑many dependency.

Do not use when:

The post‑action is fixed and never changes (over‑engineering).

Consumers have strict ordering or need each other's results (observers assume parallel independence).

6. Common pitfalls

Notification order is not guaranteed : observers are usually notified in subscription order, but code should not rely on it.

Exception propagation : a synchronous loop will stop notifying remaining observers if one throws; handle exceptions per observer or use asynchronous notification.

Memory leaks : forgetting to unsubscribe keeps the subject holding references, preventing garbage collection.

7. Summary

The Observer pattern solves the “one change, many know” problem by letting the subject maintain an observer list and broadcast changes without knowing concrete observers. It supports both push (simple, direct data transfer) and pull (flexible, on‑demand) models. Spring events, GUI listeners, and message‑queue pub‑sub are practical incarnations. Apply it when a dynamic one‑to‑many notification relationship exists, and watch out for ordering, exception handling, and memory‑leak risks.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaObserver PatternDecouplingPublish‑SubscribeSpring EventsPush vs Pull
Dabaoshi
Written by

Dabaoshi

Practical utilities

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.