Table of contents
- You Need to Send Notifications
- Structure of the Observer Pattern
- Implementing the Observer Pattern
- Observer Pattern in Practice
- The Command Pattern — Requests as Objects
- Structure of the Command Pattern
- Implementing the Command Pattern
- Extending the Command Pattern
- Observer vs Command
- Practical Pitfalls
- Key Takeaways
You Need to Send Notifications
When a product’s price changes in an e-commerce platform, multiple places need to be informed. The total in a shopping cart must be recalculated, push notifications must be sent to users who subscribed to price alerts, and dashboard statistics for admins need refreshing.
// Code that directly calls every related service on price change
public class Product {
private int price;
private CartService cartService;
private NotificationService notificationService;
private DashboardService dashboardService;
public void changePrice(int newPrice) {
this.price = newPrice;
// Every time a new service needs notifying, this code must be modified
cartService.recalculate(this);
notificationService.notifyPriceChange(this);
dashboardService.updateStats(this);
}
}
Can you see the problem? Product directly knows about CartService, NotificationService, and DashboardService. Every time a new service is added, the Product class must be modified. The domain logic of price changes and the notification logic of “who to inform” are tangled together in one class.
The Observer pattern breaks this coupling.
Structure of the Observer Pattern
The core of the Observer pattern is the Publish-Subscribe structure. The side whose state changes (Subject) announces the change, and interested parties (Observers) receive the notification and each do their own work. The Subject does not know specifically who is listening. It only needs to know the Observer interface.
Two roles participate:
- Subject: Holds state and notifies registered Observers when it changes. Provides methods to register/unregister Observers
- Observer: An interface for receiving notifications of the Subject’s state changes. Each implementation handles the notification by doing its own work
The sequence diagram below shows the Observer pattern’s workflow.
sequenceDiagram
participant Client
participant Subject
participant ObserverA
participant ObserverB
Client->>Subject: addObserver(ObserverA)
Client->>Subject: addObserver(ObserverB)
Client->>Subject: changeState()
Subject->>Subject: State change
Subject->>ObserverA: update(state)
ObserverA->>ObserverA: Execute own logic
Subject->>ObserverB: update(state)
ObserverB->>ObserverB: Execute own logic
Looking at the arrow directions, the Subject calls the Observers but does not directly know the concrete implementations (ObserverA, ObserverB). It simply calls the update() method of the Observer interface.
Implementing the Observer Pattern
Let us refactor the earlier product price change example using the Observer pattern. First, define the Observer interface and Subject.
// Observer interface that receives price change events
interface PriceObserver {
fun onPriceChanged(productId: String, oldPrice: Int, newPrice: Int)
}
The Product class, acting as the Subject, manages the Observer list and notifies all observers when the price changes.
// Subject: registers/unregisters Observers and notifies them on state change
class Product(
val id: String,
val name: String,
private var price: Int
) {
private val observers = mutableListOf<PriceObserver>()
fun addObserver(observer: PriceObserver) {
observers.add(observer)
}
fun removeObserver(observer: PriceObserver) {
observers.remove(observer)
}
fun changePrice(newPrice: Int) {
val oldPrice = this.price
this.price = newPrice
println("[$name] Price changed: $oldPrice -> $newPrice")
notifyObservers(oldPrice, newPrice)
}
private fun notifyObservers(oldPrice: Int, newPrice: Int) {
observers.forEach { it.onPriceChanged(id, oldPrice, newPrice) }
}
fun getPrice() = price
}
Now create the Observer implementations. Each handles only its own concern.
// Observer that recalculates cart totals
class CartObserver : PriceObserver {
override fun onPriceChanged(productId: String, oldPrice: Int, newPrice: Int) {
println(" [Cart] Price change detected for product $productId -> recalculating total")
}
}
// Observer that sends push notifications to users
class NotificationObserver : PriceObserver {
override fun onPriceChanged(productId: String, oldPrice: Int, newPrice: Int) {
val diff = newPrice - oldPrice
val direction = if (diff > 0) "increased" else "decreased"
println(" [Notification] Product $productId price $direction by ${Math.abs(diff)}")
}
}
// Observer that updates the admin dashboard
class DashboardObserver : PriceObserver {
override fun onPriceChanged(productId: String, oldPrice: Int, newPrice: Int) {
println(" [Dashboard] Updating price statistics for product $productId")
}
}
The client code looks like this.
fun main() {
val product = Product("P001", "Wireless Keyboard", 50000)
// Register Observers
product.addObserver(CartObserver())
product.addObserver(NotificationObserver())
product.addObserver(DashboardObserver())
// Price change -> all registered Observers are automatically notified
product.changePrice(45000)
// Output:
// [Wireless Keyboard] Price changed: 50000 -> 45000
// [Cart] Price change detected for product P001 -> recalculating total
// [Notification] Product P001 price decreased by 5000
// [Dashboard] Updating price statistics for product P001
}
Product no longer directly knows about CartService or NotificationService. It only knows the PriceObserver interface. To add a new service, just create a class implementing PriceObserver and register it. Not a single line of Product code needs modification.
Observer Pattern in Practice
The Observer pattern is already built into countless frameworks and libraries. Java’s java.util.EventListener, Spring’s @EventListener, Android’s LiveData, and JavaScript’s DOM event listeners are all variations of the Observer pattern.
Let us look at a Spring usage example.
// Spring's event system — a framework-level implementation of the Observer pattern
data class PriceChangedEvent(
val productId: String,
val oldPrice: Int,
val newPrice: Int
)
@Service
class ProductService(
private val eventPublisher: ApplicationEventPublisher
) {
fun changePrice(productId: String, newPrice: Int) {
val oldPrice = findPrice(productId)
updatePrice(productId, newPrice)
// Publish event — ProductService has no idea who is listening
eventPublisher.publishEvent(PriceChangedEvent(productId, oldPrice, newPrice))
}
}
// Each listener independently receives the event
@Component
class CartEventListener {
@EventListener
fun handle(event: PriceChangedEvent) {
println("Recalculating cart total: ${event.productId}")
}
}
@Component
class NotificationEventListener {
@EventListener
fun handle(event: PriceChangedEvent) {
println("Sending price change notification: ${event.productId}")
}
}
There is no need to manually create an Observer interface — the framework provides the publish-subscribe infrastructure. Just annotate with @EventListener and it is automatically registered as an Observer.
Observer Pattern Pitfalls
The Observer pattern has several traps.
Memory leaks. If you register an Observer but never unregister it, the Subject holds a reference to the Observer indefinitely. Even when the Observer should be eligible for garbage collection, it will not be collected. In Java, either use WeakReference or explicitly unregister in accordance with the lifecycle.
Notification order dependency. You should not write logic that depends on the order in which Observers receive notifications. In many cases, the Subject does not guarantee “who receives first.” If Observer A’s result is referenced by Observer B, you need a different approach — not the Observer pattern.
Cascading updates. If Observer A receives a notification and changes its own state, which triggers a change in another Subject, which then notifies Observer B… such chain reactions make debugging extremely difficult. It is safest to avoid modifying another Subject’s state inside a notification handler.
The Command Pattern — Requests as Objects
While Observer focuses on “notifying that state has changed,” the Command pattern focuses on “turning the request itself into an object.” The direction is somewhat different.
Suppose you are building a text editor. The user types characters, deletes them, bolds text, and hits Undo. Implementing these actions directly might look like this:
// Directly executing each action — Undo is extremely difficult
public class TextEditor {
private StringBuilder text = new StringBuilder();
public void type(String input) {
text.append(input);
}
public void delete(int count) {
text.delete(text.length() - count, text.length());
}
// How to implement Undo? Where to store the previous state?
// Each action requires different Undo logic.
}
To implement Undo, you need to record “what action was performed.” And when undoing, you must execute the inverse operation of that action. The Command pattern provides exactly this structure.
Structure of the Command Pattern
The core of the Command pattern is encapsulating a request as an object. The command “type characters” becomes an object itself, and that object contains both the execute and undo logic.
Four roles participate:
- Command: The interface for commands to execute. Defines
execute()andundo() - ConcreteCommand: The actual command implementation. Knows which method on which Receiver to call
- Receiver: The object that performs the actual work. In the text editor example,
TextEditoris the Receiver - Invoker: The object that executes commands and manages history. Manages the Undo/Redo stacks
classDiagram
class Invoker {
-history: Stack~Command~
+executeCommand(Command)
+undo()
+redo()
}
class Command {
<<interface>>
+execute()
+undo()
}
class TypeCommand {
-receiver: TextEditor
-text: String
+execute()
+undo()
}
class DeleteCommand {
-receiver: TextEditor
-count: int
-deleted: String
+execute()
+undo()
}
class BoldCommand {
-receiver: TextEditor
-range: Range
+execute()
+undo()
}
class TextEditor {
+insert(text)
+remove(count)
+applyBold(range)
}
Invoker --> Command : executes
Command <|.. TypeCommand
Command <|.. DeleteCommand
Command <|.. BoldCommand
TypeCommand --> TextEditor
DeleteCommand --> TextEditor
BoldCommand --> TextEditor
The Invoker only knows the Command interface. It does not care what specific command it is or what Receiver it manipulates. This means adding new kinds of commands does not change the Invoker code.
Implementing the Command Pattern
Let us implement Undo/Redo for the text editor using the Command pattern.
// Common interface for commands: execute and undo
interface Command {
fun execute()
fun undo()
fun description(): String
}
The Receiver TextEditor provides only basic text manipulation.
// Receiver: the actual implementation of text manipulation
class TextEditor {
private val content = StringBuilder()
fun insert(position: Int, text: String) {
content.insert(position, text)
}
fun remove(position: Int, length: Int): String {
val removed = content.substring(position, position + length)
content.delete(position, position + length)
return removed
}
fun getText(): String = content.toString()
fun length(): Int = content.length
}
Now implement the concrete commands. Starting with the type command.
// Type command: inserts text on execute, removes the inserted text on undo
class TypeCommand(
private val editor: TextEditor,
private val position: Int,
private val text: String
) : Command {
override fun execute() {
editor.insert(position, text)
}
override fun undo() {
editor.remove(position, text.length)
}
override fun description() = "Type: '$text' (position: $position)"
}
// Delete command: removes text on execute, restores deleted text on undo
class DeleteCommand(
private val editor: TextEditor,
private val position: Int,
private val length: Int
) : Command {
private var deletedText: String = "" // Remember deleted text for undo
override fun execute() {
deletedText = editor.remove(position, length)
}
override fun undo() {
editor.insert(position, deletedText)
}
override fun description() = "Delete: $length characters (position: $position)"
}
The Invoker executes commands and manages history using stacks.
// Invoker: manages command execution and Undo/Redo history
class CommandInvoker {
private val undoStack = ArrayDeque<Command>()
private val redoStack = ArrayDeque<Command>()
fun execute(command: Command) {
command.execute()
undoStack.addLast(command)
redoStack.clear() // Clear redo history when a new command is executed
println("Executed: ${command.description()}")
}
fun undo() {
if (undoStack.isEmpty()) {
println("Nothing to undo")
return
}
val command = undoStack.removeLast()
command.undo()
redoStack.addLast(command)
println("Undo: ${command.description()}")
}
fun redo() {
if (redoStack.isEmpty()) {
println("Nothing to redo")
return
}
val command = redoStack.removeLast()
command.execute()
undoStack.addLast(command)
println("Redo: ${command.description()}")
}
fun getHistory(): List<String> = undoStack.map { it.description() }
}
Let us run through the entire flow.
fun main() {
val editor = TextEditor()
val invoker = CommandInvoker()
// Type text
invoker.execute(TypeCommand(editor, 0, "Hello"))
println("Current: '${editor.getText()}'") // Hello
invoker.execute(TypeCommand(editor, 5, " World"))
println("Current: '${editor.getText()}'") // Hello World
invoker.execute(TypeCommand(editor, 11, "!"))
println("Current: '${editor.getText()}'") // Hello World!
// Undo twice
invoker.undo()
println("Current: '${editor.getText()}'") // Hello World
invoker.undo()
println("Current: '${editor.getText()}'") // Hello
// Redo
invoker.redo()
println("Current: '${editor.getText()}'") // Hello World
// Check command history
println("History: ${invoker.getHistory()}")
}
Because each command carries its own execute and undo logic, the Invoker can manage any type of command uniformly. Adding new commands (bold, indent, find-and-replace, etc.) does not change the Invoker code.
Extending the Command Pattern
The power of the Command pattern comes from the fact that “a request is an object.” Since it is an object, it can be stored, passed around, queued, and logged. Let us look at two representative patterns that leverage these properties.
Macro Commands
You can bundle multiple commands into one and execute them all at once as a macro.
// A macro command that bundles multiple commands into one
class MacroCommand(
private val commands: List<Command>
) : Command {
override fun execute() {
commands.forEach { it.execute() }
}
override fun undo() {
// Undo in reverse order to restore the original state
commands.reversed().forEach { it.undo() }
}
override fun description() = "Macro: [${commands.joinToString(", ") { it.description() }}]"
}
// Example: a macro that types "Hello World!" all at once
val macro = MacroCommand(listOf(
TypeCommand(editor, 0, "Hello"),
TypeCommand(editor, 5, " World"),
TypeCommand(editor, 11, "!")
))
invoker.execute(macro) // All three commands execute at once
invoker.undo() // All three commands are undone at once
Command Queues
Instead of executing commands immediately, you can queue them up and execute them later. This is commonly used in task schedulers and asynchronous processing.
// A structure that queues commands and executes them all later
class CommandQueue {
private val queue = ArrayDeque<Command>()
fun addCommand(command: Command) {
queue.addLast(command)
println("Added to queue: ${command.description()}")
}
fun processAll() {
println("--- Queue processing started (${queue.size} items) ---")
while (queue.isNotEmpty()) {
val command = queue.removeFirst()
command.execute()
println("Processed: ${command.description()}")
}
println("--- Queue processing complete ---")
}
}
This structure forms the foundation of message queue-based systems. If you think of messages transmitted via Kafka or RabbitMQ as “serialized Command objects,” the architecture makes natural sense.
Observer vs Command
Both patterns are closely related to “events,” but they solve different problems.
| Criterion | Observer | Command |
|---|---|---|
| Core question | Who should be notified when state changes? | How should a request be encapsulated? |
| Direction | 1:N — one Subject notifies many Observers | 1:1 — one Invoker executes one Command |
| Coupling | Subject and Observer are loosely connected | Invoker and Command are loosely connected |
| Execution timing | Immediate notification on state change | Flexible: immediate, deferred, queued, etc. |
| Undo support | Generally none | A core feature of the pattern |
| Typical use cases | Event listeners, notification systems, data binding | Undo/Redo, transactions, macros, task queues |
In practice, these two patterns are often combined. For example, you can build a structure where executing a Command also triggers notification to Observers. When a user types text (Command execution), the screen updates immediately (Observer notification) while the undo stack records it (Command history management).
Practical Pitfalls
Overusing Observer Creates Debugging Nightmares
The Observer pattern trades loose coupling for harder flow tracing. When you call product.changePrice(45000), it is hard to tell from the code alone which Observers will react. You have to search for all implementations of onPriceChanged in your IDE.
This problem is especially pronounced with Spring’s @EventListener. The event-publishing code and the receiving code are completely decoupled, so finding “who handles this event?” sometimes requires searching the entire project.
The solution is maintaining clear naming conventions and an event catalog. Keeping event classes in a single package and documenting the publishers and subscribers for each event makes tracing much easier.
Command’s Undo Is Harder Than You Think
For simple text editing, Undo is straightforward. But in practice, things get tricky. Commands that call external APIs may be irreversible, and undoing data written to a database requires compensating transactions.
When implementing Undo in the Command pattern, verify two things first. Can this command actually be reversed? And is enough information being stored at execute time to support the undo? Just as DeleteCommand stored the deleted text in deletedText, you must preserve the context needed for undo within the command object.
Key Takeaways
- The Observer pattern notifies of state changes in a 1:N fashion. Subject and Observer are loosely coupled, so adding a new Observer does not change the Subject code
- The Command pattern encapsulates a request as an object. Bundling execute and undo in one object enables features like Undo/Redo, macros, and task queues
- Observer encapsulates “who to notify,” while Command encapsulates “what to execute” — they differ in focus
- The two patterns can be combined. A structure where Command execution triggers Observer notification is a common example
- Observer makes flow tracing difficult, and Command can make Undo implementation complex — these are the trade-offs
In the next part, we look at two approaches to controlling flow. We examine Template Method, which defines the skeleton of an algorithm, and Chain of Responsibility, which passes requests along a chain.




Loading comments...