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
StoreConsolehere.
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.