Design Pattern 23: Observer Pattern - Complete Guide with Real-World Examples
π Download the complete Design Pattern series code from our design_pattern repository.
π― What is the Observer Pattern?
The Observer Pattern is a behavioral design pattern that establishes a one-to-many dependency between objects. When one object (the subject) changes its state, all its dependents (observers) are notified and updated automatically. This pattern is fundamental for implementing event-driven systems and notification mechanisms.
Key Benefits:
- β Loose coupling - Subject and observers are independent
- β Dynamic relationships - Observers can be added/removed at runtime
- β Event-driven architecture - Supports reactive programming
- β Scalability - Easy to add new observers without modifying subject
- β Real-time updates - Automatic notification when state changes
π Real-World Problem: Security System Notification
Letβs design a security system host (Panel) with the following requirements:
System Requirements:
- Host monitors various sensors (smoke detectors, door/window sensors)
- Automatic notification to all registered devices when alarms trigger
- Dynamic device management - devices can join/leave notification list
- Multi-platform support - tablets, iOS, and Android devices
Business Rules:
- Host must notify all registered devices simultaneously
- Devices can be added or removed without affecting other devices
- Different device types may handle notifications differently
- System should be extensible for new device types
ποΈ Object-Oriented Analysis (OOA)
Letβs analyze the problem and identify the core components:

Identified Forces:
- High Coupling
- Direct interaction between host and each device creates tight coupling
- Adding/removing devices requires modifying host logic
- Lack of Flexibility
- Adding new devices violates Open-Closed Principle (OCP)
- Hard to maintain as system grows
- Inconsistent Notifications
- Difficult to ensure all devices receive notifications properly
- No standardized notification mechanism
π‘ Observer Pattern Solution
After analyzing the forces, we can apply the Observer Pattern to create a flexible notification system:

Observer Pattern Components:
- Subject Interface
- Defines methods for managing observers
- Provides notification mechanism
- Observer Interface
- Defines update method for observers
- Ensures consistent notification handling
- Concrete Subject
- Implements subject interface
- Manages observer collection and notifications
- Concrete Observers
- Implement observer interface
- Handle specific notification logic
Benefits:
- Loose coupling between subject and observers
- Dynamic observer management at runtime
- Consistent notification mechanism
π οΈ Implementation: Security System Notification
Hereβs the complete implementation using the Observer Pattern:

1. Subject Interface
interface AlarmSystem {
fun addObserver(observer: Device)
fun removeObserver(observer: Device)
fun notifyObservers(alarmMessage: String)
fun getObserverCount(): Int
}
2. Observer Interface
interface Device {
fun onAlarmTriggered(alarmMessage: String)
fun getDeviceId(): String
}
3. Concrete Subject Implementation
class SecurityPanel : AlarmSystem {
private val devices = mutableListOf<Device>()
private var alarmCount = 0
override fun addObserver(observer: Device) {
if (!devices.contains(observer)) {
devices.add(observer)
println("π± Device ${observer.getDeviceId()} registered for notifications")
}
}
override fun removeObserver(observer: Device) {
if (devices.remove(observer)) {
println("β Device ${observer.getDeviceId()} unregistered from notifications")
}
}
override fun notifyObservers(alarmMessage: String) {
println("π¨ Broadcasting alarm to ${devices.size} devices...")
devices.forEach { device ->
try {
device.onAlarmTriggered(alarmMessage)
} catch (e: Exception) {
println("β οΈ Failed to notify ${device.getDeviceId()}: ${e.message}")
}
}
}
override fun getObserverCount(): Int = devices.size
fun triggerAlarm(zone: String, severity: AlarmSeverity = AlarmSeverity.MEDIUM) {
alarmCount++
val message = "π¨ ALARM #$alarmCount: $severity alert in $zone!"
println("π Security Panel: $message")
notifyObservers(message)
}
fun getSystemStatus(): String {
return "Security Panel Status: ${devices.size} devices registered, $alarmCount alarms triggered"
}
}
enum class AlarmSeverity {
LOW, MEDIUM, HIGH, CRITICAL
}
4. Concrete Observer Implementations
class Tablet : Device {
private val deviceId = "Tablet-${System.currentTimeMillis() % 1000}"
override fun onAlarmTriggered(alarmMessage: String) {
println("π± Tablet ($deviceId): Displaying alert - $alarmMessage")
// Simulate tablet-specific notification
println(" πΊ Showing full-screen alert on tablet display")
}
override fun getDeviceId(): String = deviceId
}
class IOSDevice : Device {
private val deviceId = "iOS-${System.currentTimeMillis() % 1000}"
override fun onAlarmTriggered(alarmMessage: String) {
println("π iOS Device ($deviceId): Push notification - $alarmMessage")
// Simulate iOS-specific notification
println(" π± Sending APNS push notification")
println(" π Playing iOS notification sound")
}
override fun getDeviceId(): String = deviceId
}
class AndroidDevice : Device {
private val deviceId = "Android-${System.currentTimeMillis() % 1000}"
override fun onAlarmTriggered(alarmMessage: String) {
println("π€ Android Device ($deviceId): FCM notification - $alarmMessage")
// Simulate Android-specific notification
println(" π± Sending FCM push notification")
println(" π Playing Android notification sound")
println(" π³ Triggering vibration")
}
override fun getDeviceId(): String = deviceId
}
5. Client Code
fun main() {
println("=== Security System Observer Pattern Demo ===")
val securityPanel = SecurityPanel()
// Create different device types
val tablet = Tablet()
val iosDevice = IOSDevice()
val androidDevice = AndroidDevice()
// Register devices as observers
securityPanel.addObserver(tablet)
securityPanel.addObserver(iosDevice)
securityPanel.addObserver(androidDevice)
println("\n--- Testing Alarm Notifications ---")
// Trigger alarms in different zones
securityPanel.triggerAlarm("Living Room", AlarmSeverity.MEDIUM)
securityPanel.triggerAlarm("Kitchen", AlarmSeverity.HIGH)
// Remove one observer
securityPanel.removeObserver(androidDevice)
// Trigger another alarm
securityPanel.triggerAlarm("Bedroom", AlarmSeverity.LOW)
// Add a new device
val newTablet = Tablet()
securityPanel.addObserver(newTablet)
// Final alarm test
securityPanel.triggerAlarm("Garage", AlarmSeverity.CRITICAL)
println("\n--- System Status ---")
println(securityPanel.getSystemStatus())
}
Expected Output:
=== Security System Observer Pattern Demo ===
π± Device Tablet-123 registered for notifications
π± Device iOS-456 registered for notifications
π± Device Android-789 registered for notifications
--- Testing Alarm Notifications ---
π Security Panel: π¨ ALARM #1: MEDIUM alert in Living Room!
π¨ Broadcasting alarm to 3 devices...
π± Tablet (Tablet-123): Displaying alert - π¨ ALARM #1: MEDIUM alert in Living Room!
πΊ Showing full-screen alert on tablet display
π iOS Device (iOS-456): Push notification - π¨ ALARM #1: MEDIUM alert in Living Room!
π± Sending APNS push notification
π Playing iOS notification sound
π€ Android Device (Android-789): FCM notification - π¨ ALARM #1: MEDIUM alert in Living Room!
π± Sending FCM push notification
π Playing Android notification sound
π³ Triggering vibration
π Security Panel: π¨ ALARM #2: HIGH alert in Kitchen!
π¨ Broadcasting alarm to 3 devices...
[... similar output for other devices ...]
β Device Android-789 unregistered from notifications
π Security Panel: π¨ ALARM #3: LOW alert in Bedroom!
π¨ Broadcasting alarm to 2 devices...
[... output for remaining devices ...]
π± Device Tablet-987 registered for notifications
π Security Panel: π¨ ALARM #4: CRITICAL alert in Garage!
π¨ Broadcasting alarm to 3 devices...
[... output for all devices ...]
--- System Status ---
Security Panel Status: 3 devices registered, 4 alarms triggered
π§ Advanced Implementation: Enhanced Observer Pattern
Letβs create a more sophisticated version with filtering and priority support:
// Enhanced observer with filtering capabilities
interface EnhancedDevice : Device {
fun getNotificationPreferences(): NotificationPreferences
fun canHandleSeverity(severity: AlarmSeverity): Boolean
}
data class NotificationPreferences(
val minSeverity: AlarmSeverity = AlarmSeverity.LOW,
val zones: Set<String> = setOf(),
val enableSound: Boolean = true,
val enableVibration: Boolean = true
)
class EnhancedSecurityPanel : AlarmSystem {
private val devices = mutableListOf<EnhancedDevice>()
override fun addObserver(observer: Device) {
if (observer is EnhancedDevice) {
devices.add(observer)
}
}
override fun removeObserver(observer: Device) {
devices.remove(observer as? EnhancedDevice)
}
override fun notifyObservers(alarmMessage: String) {
// Enhanced notification with filtering
devices.filter { device ->
device.canHandleSeverity(extractSeverity(alarmMessage))
}.forEach { device ->
device.onAlarmTriggered(alarmMessage)
}
}
private fun extractSeverity(message: String): AlarmSeverity {
return when {
message.contains("CRITICAL") -> AlarmSeverity.CRITICAL
message.contains("HIGH") -> AlarmSeverity.HIGH
message.contains("MEDIUM") -> AlarmSeverity.MEDIUM
else -> AlarmSeverity.LOW
}
}
override fun getObserverCount(): Int = devices.size
}
π Observer Pattern vs Alternative Approaches
Approach | Pros | Cons |
---|---|---|
Observer Pattern | β
Loose coupling β Dynamic relationships β Event-driven | β Potential memory leaks β Unordered notifications |
Polling | β Simple implementation | β Resource intensive β Delayed updates |
Direct References | β Fast execution | β Tight coupling β Hard to maintain |
Event Bus | β Decoupled communication | β Complex debugging β Global state |
π― When to Use the Observer Pattern
β Perfect For:
- Event-driven systems (GUI frameworks, game engines)
- Notification systems (push notifications, alerts)
- Model-View architectures (MVC, MVP)
- Real-time updates (stock tickers, chat applications)
- Plugin architectures (extensible systems)
β Avoid When:
- Simple one-to-one relationships (use direct calls)
- Performance-critical systems (notification overhead)
- Order-dependent operations (observers execute in undefined order)
- Memory-constrained environments (potential memory leaks)
π Related Design Patterns
- Mediator Pattern: Can coordinate multiple observers
- Command Pattern: Can encapsulate observer actions
- Chain of Responsibility: Alternative for event handling
- Event Sourcing: For complex event-driven architectures
π Real-World Applications
1. GUI Frameworks
// Button click observers
interface ButtonClickListener {
fun onClick(button: Button)
}
class Button {
private val listeners = mutableListOf<ButtonClickListener>()
fun addClickListener(listener: ButtonClickListener) {
listeners.add(listener)
}
fun click() {
listeners.forEach { it.onClick(this) }
}
}
2. Stock Market Applications
interface StockObserver {
fun onPriceChange(symbol: String, price: Double)
}
class StockMarket {
private val observers = mutableListOf<StockObserver>()
fun updatePrice(symbol: String, price: Double) {
observers.forEach { it.onPriceChange(symbol, price) }
}
}
3. Social Media Notifications
interface NotificationObserver {
fun onNewPost(userId: String, content: String)
fun onLike(postId: String, userId: String)
}
class SocialMediaPlatform {
private val followers = mutableMapOf<String, MutableList<NotificationObserver>>()
fun addFollower(userId: String, observer: NotificationObserver) {
followers.getOrPut(userId) { mutableListOf() }.add(observer)
}
}
4. IoT Device Management
interface SensorObserver {
fun onSensorReading(sensorId: String, value: Double, timestamp: Long)
}
class IoTHub {
private val sensorObservers = mutableListOf<SensorObserver>()
fun sensorReading(sensorId: String, value: Double) {
sensorObservers.forEach {
it.onSensorReading(sensorId, value, System.currentTimeMillis())
}
}
}
π¨ Common Pitfalls and Best Practices
1. Memory Leaks
// β Avoid: Observers not properly removed
class BadSubject {
private val observers = mutableListOf<Observer>()
fun addObserver(observer: Observer) {
observers.add(observer) // Observer might not be removed
}
}
// β
Prefer: Weak references or proper cleanup
class GoodSubject {
private val observers = mutableListOf<WeakReference<Observer>>()
fun addObserver(observer: Observer) {
observers.add(WeakReference(observer))
}
fun cleanup() {
observers.removeAll { it.get() == null }
}
}
2. Notification Order
// β Avoid: Unpredictable notification order
override fun notifyObservers(message: String) {
observers.forEach { it.update(message) } // Order undefined
}
// β
Prefer: Defined notification order
override fun notifyObservers(message: String) {
observers.sortedBy { it.priority }.forEach { it.update(message) }
}
3. Exception Handling
// β
Good: Handle observer exceptions gracefully
override fun notifyObservers(message: String) {
observers.forEach { observer ->
try {
observer.update(message)
} catch (e: Exception) {
logger.error("Observer notification failed", e)
// Optionally remove failed observer
observers.remove(observer)
}
}
}
π Related Articles
- Design Pattern 1: Object-Oriented Concepts
- Design Pattern 2: Design Principles
- State Pattern
- Strategy Pattern
- Command Pattern
β Conclusion
Through the Observer Pattern, we successfully built a flexible security system notification mechanism that allows devices to dynamically join or leave while maintaining loose coupling and following the Open-Closed Principle (OCP).
Key Advantages:
- π― Loose coupling - Subject and observers are independent
- π§ Dynamic relationships - Observers can be added/removed at runtime
- π Scalability - Easy to add new observers without modifying subject
- π‘οΈ Consistent notifications - Standardized notification mechanism
- β‘ Event-driven architecture - Supports reactive programming
Design Principles Followed:
- Single Responsibility Principle (SRP): Each observer handles its own notification logic
- Open-Closed Principle (OCP): Open for extension (new observers), closed for modification
- Dependency Inversion Principle (DIP): Depend on abstractions, not concretions
Perfect For:
- Real-time alert systems (security, monitoring)
- Message push systems (notifications, updates)
- Event distribution systems (logging, analytics)
- GUI frameworks (button clicks, form changes)
- Plugin architectures (extensible applications)
The Observer Pattern provides an elegant solution for event-driven communication and is essential for building responsive, scalable systems!
π‘ Pro Tip: Consider using WeakReferences for observers to prevent memory leaks, especially in long-running applications.
π Stay Updated: Follow our Design Pattern series for more software architecture insights!
Enjoy Reading This Article?
Here are some more articles you might like to read next: