Table of Contents
KEY TAKEAWAYS
- The Observer pattern decouples event producers from consumers using callback functions
- Modules register their own handlers — the event source never changes
- Ideal for ISRs, sensor alerts, and any one-to-many notification
- Keep the number of observers small and use named functions for debuggability
What Is the Observer Pattern?
The Observer pattern lets one part of your code notify other parts when something happens, without knowing who those other parts are. In C, this is implemented using callback functions — function pointers that get called when an event occurs.Think of it like a mailing list: subscribers register their interest, and when news arrives, everyone on the list gets notified. The publisher does not need to know who the subscribers are.Example 1: Button Press Handler
Bad — Hardcoded Responses
void button_isr(void) {
// Directly calls every module that cares about the button
led_toggle();
buzzer_beep();
display_update();
log_event("button pressed");
// New feature? Edit this ISR again.
}The button ISR knows about LEDs, buzzers, displays, and logging. Adding any new response requires modifying the interrupt handler — risky in safety-critical code.Good — Callback Registration
typedef void (*ButtonCallback)(void);
#define MAX_CALLBACKS 8
static ButtonCallback callbacks[MAX_CALLBACKS];
static int cb_count = 0;
void button_register(ButtonCallback cb) {
if (cb_count < MAX_CALLBACKS) {
callbacks[cb_count++] = cb;
}
}
void button_isr(void) {
for (int i = 0; i < cb_count; i++) {
callbacks[i]();
}
}
// Each module registers itself — button ISR never changes
void app_init(void) {
button_register(led_toggle);
button_register(buzzer_beep);
button_register(display_update);
}The button module is completely decoupled from its observers. Each module registers its own callback. The ISR stays small and stable. Adding a new response means one button_register() call — zero changes to existing code.Example 2: Sensor Threshold Alerts
Bad — Sensor Module Knows All Consumers
void sensor_read_cycle(void) {
float temp = read_temperature();
// Sensor module directly calls everyone
if (temp > 80.0f) {
cooling_activate();
alarm_trigger(ALARM_OVERTEMP);
network_send_alert(temp);
lcd_show_warning(temp);
}
}Good — Event Notification System
typedef void (*TempAlertFn)(float temperature);
#define MAX_WATCHERS 8
static TempAlertFn watchers[MAX_WATCHERS];
static int watcher_count = 0;
static float threshold = 80.0f;
void sensor_on_overtemp(TempAlertFn fn) {
if (watcher_count threshold) {
for (int i = 0; i < watcher_count; i++) {
watchers[i](temp);
}
}
}
// Modules subscribe independently
void system_init(void) {
sensor_on_overtemp(cooling_activate_cb);
sensor_on_overtemp(alarm_trigger_cb);
sensor_on_overtemp(network_alert_cb);
}The sensor module only knows that callbacks exist — it does not know about cooling, alarms, or networking. Each module owns its own response logic. You can add or remove watchers without touching the sensor code.When to Use the Observer Pattern
- Event-driven systems — interrupts, button presses, timer expirations
- Decoupling producers from consumers — a sensor module should not know about the display module
- Plugin-style architecture — let modules register behaviors at startup
- Multiple reactions to one event — when several parts of the system need to respond
When NOT to Use It
- Single consumer — if only one function ever responds to the event, a direct call is simpler
- Order matters — observers are called in registration order, which can be fragile. If ordering is critical, consider a pipeline pattern instead
- Debugging complexity — callbacks can make control flow hard to trace. Use named functions (not anonymous pointers) and keep the callback count manageable
Key Takeaways
- The Observer pattern decouples event producers from consumers using callback functions
- Modules register their own handlers — the event source never changes
- Ideal for ISRs, sensor alerts, and any one-to-many notification
- Keep the number of observers small and use named functions for debuggability

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.







