When an order ships, a lot of things should happen: tell the customer, update their loyalty points, record it for analytics. The question is whether the order service should know about all of those things. Observer says no: the order service announces what happened, and whoever cares can listen.

   
Category Behavioral
Smell fixed S19: Order transitions call every side-effect service
Code patterns/behavioral/observer

The Scene

Jo’s order ships, and three things happen at once:

[067] order.ship order=o-1002
  sms to Jo: order o-1002 shipped, tracking SF-0002
  loyalty updated for Jo (Shipped)
  analytics order o-1002 Shipped
  order o-1002 shipped

A notification, a loyalty update and an analytics record. Next quarter, the business will want a fourth reaction: maybe a warehouse dashboard, maybe a customer review request. How hard is it to add?


The Smell

In the baseline, every order transition funnels into one method that calls each side effect directly:

    private void RecordStatusChange(Order order)
    {
        // SMELL S19: order transitions directly call stock, notification, loyalty and analytics services
        foreach (var line in order.Lines)
        {
            inventory.GetStock(line.Product.Sku);
        }

        if (order.Status == "Paid")
        {

It goes on to pick the right notification and ends with:

loyalty.RecordStatusChange(order);
analytics.RecordStatusChange(order);

The order service’s constructor tells the story. It needs eight collaborators: ID generation, inventory, four notification classes, loyalty and analytics. Most of them have nothing to do with managing an order. They’re just things that want to know when an order changes.

Every new reaction means editing OrderService and adding yet another constructor parameter. The class responsible for order rules keeps growing because of features that aren’t order rules.


The Idea

Observer separates the thing that changes (the subject) from the things that react (the observers). Observers subscribe to the subject. When something changes, the subject notifies every subscriber through one common interface. It doesn’t know or care what each one does.

Think of a newsletter. The writer publishes an issue once. Whoever subscribed gets it. The writer doesn’t need a list of what each reader does with it, and new readers can sign up without the writer changing anything.


The Refactor

Every reaction implements one tiny interface:

internal interface IOrderStatusObserver
{
    void OnStatusChanged(Order order);
}

Each observer owns exactly one reaction. Here’s loyalty:

internal sealed class LoyaltyOrderStatusObserver(LoyaltyService loyalty) : IOrderStatusObserver
{
    public void OnStatusChanged(Order order)
    {
        loyalty.RecordStatusChange(order);
    }
}

The order service just keeps a list and notifies it:

private void NotifyObservers(Order order)
{
    foreach (var observer in observers)
    {
        observer.OnStatusChanged(order);
    }
}

And the wiring happens once, in StoreConsole, where the app is put together:

        orderService = new OrderService(Ids, inventory);
        orderService.Subscribe(new InventoryOrderStatusObserver(inventory));

Look at that constructor: OrderService(Ids, inventory). Eight dependencies down to two. The order service is back to being about orders.

classDiagram
    StoreConsole ..> OrderService : registers observers
    OrderService --> IOrderStatusObserver : notifies
    IOrderStatusObserver <|.. InventoryOrderStatusObserver
    IOrderStatusObserver <|.. NotificationOrderStatusObserver
    IOrderStatusObserver <|.. LoyaltyOrderStatusObserver
    IOrderStatusObserver <|.. AnalyticsOrderStatusObserver

Order still matters

Here’s a subtle point the golden master caught. The transcript always prints the notification first, then loyalty, then analytics. So the observers have to be subscribed in exactly that order.

Observer is often described as “the subject doesn’t care who’s listening”. True. But the system may still care about the order things happen in. If your observers depend on running in sequence, make that explicit and test it, like the golden master does here. Don’t leave it to chance.


What It Cost

9 files: about 193 lines added and 48 removed. Four small observer classes and an interface. The notification observer still uses the four separate message classes from smell S07, because untangling those is Bridge’s job.


When Not to Use It

  • There’s only one reaction, and there always will be. A direct call is easier to follow than a subscription.
  • You need to know what happened. The subject fires and forgets. If an observer fails, should the order fail too? Observer doesn’t answer that for you, and in real systems it’s an important design decision.
  • Debugging gets harder. With direct calls, “go to definition” shows you exactly what runs. With observers, you have to find where they were subscribed. Keep subscription in one obvious place, like StoreConsole here.

The .NET way

C# has Observer built into the language: the event keyword with EventHandler<T>. For streams of values over time, there’s IObservable<T> and IObserver<T>, which Reactive Extensions builds on. CornerStore uses an explicit interface so the subscription order and the data being passed are easy to see, but in everyday C# an event is often the simplest choice.


Easily Confused With

Mediator. Both decouple senders from receivers. Observer broadcasts to whoever subscribed, and the subject doesn’t decide what happens next. A mediator actively coordinates a known set of colleagues and decides who reacts and how. See the Mediator post for the contrast.


Try It Yourself

dotnet build patterns/behavioral/observer/Observer.slnx
dotnet test patterns/behavioral/observer/Observer.slnx
dotnet run --project patterns/behavioral/observer/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json

Exercise: write a WarehouseOrderStatusObserver that logs a line when an order ships, and subscribe it. OrderService doesn’t change at all. Then swap the subscription order of two observers and watch the golden master fail.

The Observer README has the participant table and a sequence diagram. The full change is in DIFF.md.


Previous: Memento · Next: State