Introduction
Complex embedded systems quickly develop messy interaction graphs: sensors trigger logic that wakes radios, which log to flash, while power management tries to keep the node asleep. Left unchecked, these interactions become brittle, inefficient, and hard to test.
The Mediator Pattern solves this by introducing a single coordinator — the mediator — that all modules talk to. Instead of N×N direct couplings, modules publish requests or events to the mediator and the mediator orchestrates sequencing, arbitration, batching and policy enforcement.
In low-power systems the mediator is especially powerful: it can align sensor reads to radio wake windows, batch transfers to save wakeups, and centrally enforce power budgets.
What is the Mediator Pattern?
A mediator is a software component that controls interactions among a set of modules. Modules communicate with each other through the mediator rather than directly. The mediator:
- centralizes coordination and policies,
- enforces resource arbitration (SPI/I²C, radios, DMA),
- batches or aligns activities for power efficiency,
- keeps modules decoupled and testable.
Think of it as “air traffic control” for device subsystems.
When to Use It
Use a mediator when:
- multiple subsystems must coordinate (sensor fusion, radio + sensor scheduling),
- global policies must be enforced (power budgets, QoS),
- timing alignment yields energy savings,
- shared resources require arbitration.
Avoid it for tiny, isolated modules that do not interact.
Benefits & Drawbacks
Benefits: loose coupling, centralized policies, easier system-level testing, energy optimizations via batching.
Drawbacks: may become a bottleneck or “god object” if overloaded; adds indirection and possible latency.
Concurrency & ISR Rules (Practical)
- ISRs should signal the mediator (enqueue event), not perform coordination.
- Mediator logic should run in task/thread or main loop context.
- Use lock-free or RTOS ISR-safe primitives for ISR → mediator signaling.
- Keep mediator non-blocking where possible — offload heavy work to worker tasks.
Example — sensor fusion + radio mediator
Below are two small examples showing the same idea:
- RTOS variant (FreeRTOS-like): mediator runs as a task; ISRs enqueue events with
xQueueSendFromISR
. - Bare-metal variant: mediator uses a lightweight SPSC ring buffer and runs in the main loop.
Both examples show:
- ISR → mediator signaling,
- mediator requesting another sensor read,
- align/fuse data,
- command radio transmission.
These snippets are intentionally compact to illustrate structure; production code should add error handling, full type checks, timeouts, watchdog interaction, and unit tests.
Design Variants
The Mediator Pattern can be implemented in several flavors depending on system constraints and goals. Here are common design variants you can pick from or combine.
1. Centralized Scheduler Mediator
A single task or state machine acts as the authoritative scheduler. Modules register callbacks or events with the mediator; the mediator enforces ordering, arbitration, and policies.
- When to use: RTOS-based systems with clear timing needs (e.g., sensor hub that samples multiple sensors and transmits periodically).
- Pros: Simple to reason about, straightforward scheduling and resource arbitration.
- Cons: Single point of failure; risk of becoming a bottleneck if it handles heavy work.
2. Event Bus Mediator
Mediator behaves as a publish/subscribe hub: modules publish events to the mediator, which routes or forwards messages to subscribers while applying global policies.
- When to use: Systems where many producers and many consumers exist (e.g., telemetry + logging + analytics consumers of the same sensors).
- Pros: Flexible, extensible, decouples producers from consumers.
- Cons: Requires subscription lifecycle management and may need backpressure handling.
3. Policy-Driven Mediator
Mediator encapsulates higher-level policies (power budgets, deadlines, QoS) and makes decisions based on those constraints instead of only event sequencing.
- When to use: Power-sensitive IoT nodes or safety-critical systems where system-level constraints govern behavior.
- Pros: Centralized policy enforcement enables consistent system-wide optimizations.
- Cons: More complex to design and test; policy changes may have wide-ranging effects.
4. Hierarchical Mediator
Split responsibilities across multiple mediators organized in a hierarchy: local mediators handle a small subsystem; a top-level mediator coordinates across the locals.
- When to use: Large embedded platforms (e.g., gateway devices, multi-domain SoCs) where a single mediator would be too complex.
- Pros: Limits scope of each mediator, reduces risk of a single god object.
- Cons: Introduces additional coordination between mediators and slight added latency.
5. Hybrid Proxy–Mediator
Combine Hardware Proxy and Mediator: proxies abstract individual peripherals while a mediator orchestrates how multiple proxies interact (e.g., when to power them, when to batch transfers).
- When to use: Systems where device-specific access details must be hidden and subsystems still need global coordination.
- Pros: Clear separation of concerns; proxies keep driver code clean while mediator handles sequencing.
- Cons: More layers to design and integrate; need clear interface contracts.
Comparison with Related Patterns
To help you pick the right pattern for each problem, here is a concise comparison of the Mediator Pattern with the Hardware Proxy and other common hardware-access patterns discussed in this series.
Pattern Primary Purpose When to Prefer Pros Cons Mediator Coordinate interactions between modules/subsystems Multiple modules must be orchestrated, global policies, timing alignment Decouples modules, centralizes policies, enables batching and power optimization Can become a monolithic “god object”, adds indirection Hardware Proxy Encapsulate a single hardware block behind a safe API Complex peripheral access or vendor quirks; transformation required Hides registers/protocols, testable, portable Adds indirection; proxy could become a bottleneck Adapter Translate between incompatible interfaces Reusing third-party components or legacy APIs Promotes reuse, adapts mismatched contracts Adds glue code, hides incompatibilities Observer Distribute events/data to multiple subscribers Many consumers need the same data (e.g., sensor + logger + comms) Efficient fan-out, decoupled subscribers Needs careful lifecycle and ordering management Command Queue + Worker Serialize and batch hardware requests via a worker High-concurrency access to shared peripherals Centralized arbitration, keeps ISRs short Queue memory, scheduling latency Typical co-usage patterns:
- Proxy + Mediator: proxy isolates a device and provides a clean interface; mediator decides when to use that device (power/capacity/policy). This is the most common and powerful combination for sustainable embedded systems.
- Mediator + Observer: use mediator for sequencing and global policies, while observer pattern fans out events from the mediator to multiple subscribers (e.g., logging, telemetry, analytics).
- Mediator + Command Queue: the mediator enqueues commands onto bounded queues processed by workers or drivers — useful when heavy transforms or DMA are required.
RTOS Example (FreeRTOS-style)
// mediator_rtos.c // Compact RTOS-style mediator coordinating accel, mag, radio. // Assumes FreeRTOS-like API for queues and tasks. #include "FreeRTOS.h" #include "task.h" #include "queue.h" #include <stdint.h> #include <stdbool.h> typedef enum { EVT_ACCEL_READY, EVT_MAG_READY, EVT_RADIO_TX_DONE, EVT_TIMER_TICK, } mediator_event_t; typedef struct { mediator_event_t type; void *payload; } mediator_msg_t; static QueueHandle_t mediator_q; // Mock module interfaces (implementations elsewhere) void accel_start_sample(void); void mag_start_sample(void); void radio_send_packet(const uint8_t *buf, size_t len); bool get_accel_data(int32_t *out); // returns true if sample available bool get_mag_data(int32_t *out); static void mediator_task(void *arg) { mediator_msg_t msg; int32_t accel_sample = 0, mag_sample = 0; bool have_accel = false, have_mag = false; for (;;) { if (xQueueReceive(mediator_q, &msg, portMAX_DELAY) == pdTRUE) { switch (msg.type) { case EVT_ACCEL_READY: // mark and request magnetometer for fusion have_accel = get_accel_data(&accel_sample); if (!have_mag) { mag_start_sample(); // request other sensor } break; case EVT_MAG_READY: have_mag = get_mag_data(&mag_sample); if (!have_accel) { accel_start_sample(); // ensure accel present (policy) } break; case EVT_RADIO_TX_DONE: // could free buffers, update power policy break; case EVT_TIMER_TICK: // periodic housekeeping: maybe flush/aggregate break; } if (have_accel && have_mag) { // Simple "fusion" example (really just pack two ints) uint8_t packet[8]; memcpy(packet + 0, &accel_sample, sizeof(accel_sample)); memcpy(packet + 4, &mag_sample, sizeof(mag_sample)); radio_send_packet(packet, sizeof(packet)); // reset flags; we will wait for next events have_accel = have_mag = false; } } } } // ISR handlers (hardware-specific) should do minimal work: enqueue event. void accel_isr_handler(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; mediator_msg_t m = { .type = EVT_ACCEL_READY, .payload = NULL }; xQueueSendFromISR(mediator_q, &m, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } void mag_isr_handler(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; mediator_msg_t m = { .type = EVT_MAG_READY, .payload = NULL }; xQueueSendFromISR(mediator_q, &m, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } int main(void) { // init hardware... mediator_q = xQueueCreate(16, sizeof(mediator_msg_t)); xTaskCreate(mediator_task, "mediator", 1024, NULL, tskIDLE_PRIORITY+2, NULL); vTaskStartScheduler(); for(;;); }
Bare-Metal Example (SPSC Ring Buffer + Main Loop)
// mediator_baremetal.c // Minimal bare-metal mediator using SPSC ring buffer. #include <stdint.h> #include <stdbool.h> #include <string.h> #define EVT_RING_SIZE 32 typedef enum { EVT_NONE=0, EVT_ACCEL_READY, EVT_MAG_READY, EVT_SEND_TIMER } evt_t; typedef struct { volatile uint8_t head; volatile uint8_t tail; evt_t buf[EVT_RING_SIZE]; } evt_ring_t; static evt_ring_t evt_ring = {.head=0, .tail=0}; static inline bool evt_ring_put(evt_t e) { uint8_t next = (evt_ring.head + 1) & (EVT_RING_SIZE - 1); if (next == evt_ring.tail) return false; // full evt_ring.buf[evt_ring.head] = e; evt_ring.head = next; return true; } static inline bool evt_ring_get(evt_t *e) { if (evt_ring.tail == evt_ring.head) return false; // empty *e = evt_ring.buf[evt_ring.tail]; evt_ring.tail = (evt_ring.tail + 1) & (EVT_RING_SIZE - 1); return true; } // ISR context: push events into ring (ensure power-of-two size) void accel_isr(void) { evt_ring_put(EVT_ACCEL_READY); } void mag_isr(void) { evt_ring_put(EVT_MAG_READY); } void timer_isr(void) { evt_ring_put(EVT_SEND_TIMER); } void accel_start_sample(void); void mag_start_sample(void); void radio_send_packet(const uint8_t *buf, size_t len); bool get_accel_data(int32_t *out); bool get_mag_data(int32_t *out); int main(void) { int32_t accel = 0, mag = 0; bool have_accel=false, have_mag=false; // init hardware, enable interrupts... while (1) { evt_t ev; // run mediator loop: service events while (evt_ring_get(&ev)) { switch (ev) { case EVT_ACCEL_READY: have_accel = get_accel_data(&accel); if (!have_mag) mag_start_sample(); break; case EVT_MAG_READY: have_mag = get_mag_data(&mag); if (!have_accel) accel_start_sample(); break; case EVT_SEND_TIMER: // periodic flush/housekeeping break; default: break; } if (have_accel && have_mag) { uint8_t pkt[8]; memcpy(pkt+0, &accel, 4); memcpy(pkt+4, &mag, 4); radio_send_packet(pkt, sizeof(pkt)); have_accel = have_mag = false; } } // Do low-priority tasks or enter low-power sleep until IRQ __WFI(); // wait for interrupt (ARM) } }
Best Practices & Checklist
- Design mediator API with abstract interfaces so modules remain testable and swappable.
- Keep ISRs minimal: enqueue and return.
- Prefer bounded queues/rings to avoid memory growth.
- Separate policy from low-level mechanics (e.g., keep scheduling rules in readable modules).
- Add capability discovery if modules have optional features.
- Avoid making mediator a catch-all: split responsibilities if it grows too large.
- Provide test hooks and mocks for CI-driven system-level tests.
- Monitor mediator metrics (latency, queue drops, conflicts) at runtime.
Anti-Patterns
- Modules directly calling each other instead of the mediator.
- Dumping all logic into the mediator (creating a gigantic “god object”).
- Doing heavy work in ISRs or blocking the mediator task.
- Unbounded, uncontrolled queues that cause memory exhaustion.
Reference:
Design Patterns for Embedded Systems in C by Bruce Powel Douglass
Conclusion
The Mediator Pattern brings order to inter-module complexity. For embedded systems — where power, timing, and resource contention matter — mediators let you encode cross-cutting policies in one place, align activity for energy savings, and keep modules decoupled and testable.
Use mediators alongside patterns like the Hardware Proxy: proxies simplify how individual devices are used; mediators coordinate when and why devices are used together.