Skip to content
Home » Software Design » Observer Pattern (Callbacks) in C — With Practical Examples

Observer Pattern (Callbacks) in C — With Practical Examples

Observer Pattern featured image with purple background, Design Patterns badge, Ob icon, Callbacks in C subtitle in the Software Design in C series by NerdyElectronics
Software Design Principles in C
Part 13 of 31View Full Path →

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

Leave a Reply

Your email address will not be published. Required fields are marked *