Systems Architecture

The Init-Update-Status-Reset Pattern: O(1) Guarantees for Safety Monitors

A four-function interface that enables static analysis, bounded resources, and compositional verification

Published
January 26, 2026 20:05
Reading Time
10 min
Diagram showing the Init-Update-Status-Reset lifecycle pattern with four functions forming a cycle and associated guarantees

Every module in the c-from-scratch framework implements the same four-function interface:

int module_init(module_t *ctx, const module_config_t *cfg);
int module_update(module_t *ctx, input_t input, uint64_t timestamp);
module_state_t module_status(const module_t *ctx);
void module_reset(module_t *ctx);

This isn’t arbitrary convention. The Init-Update-Status-Reset pattern is the minimal interface that simultaneously enables static analysis, bounded resource usage, deterministic execution, and compositional verification.

Each function has a specific role in the module lifecycle, and the boundaries between them are load-bearing. Blur those boundaries and the guarantees collapse.

The Four Functions

init(): Configure Without Allocating

int baseline_init(baseline_t *ctx, const baseline_config_t *cfg) {
    if (ctx == NULL || cfg == NULL) {
        return BASELINE_ERR_NULL;
    }
    if (cfg->alpha <= 0.0 || cfg->alpha > 1.0) {
        return BASELINE_ERR_CONFIG;
    }
    
    ctx->cfg = *cfg;  // Copy configuration
    ctx->state = BASELINE_LEARNING;
    ctx->sample_count = 0;
    ctx->ema = 0.0;
    ctx->variance = 0.0;
    ctx->last_timestamp = 0;
    
    return BASELINE_OK;
}

What init() does:

  • Validates configuration parameters
  • Copies configuration into the context (caller owns the memory)
  • Sets initial state
  • Zeroes runtime fields

What init() doesn’t do:

  • Allocate memory (caller provides the context)
  • Perform any I/O
  • Depend on external state
  • Do anything that could fail unpredictably

The caller owns memory allocation:

// Stack allocation - automatic lifetime
baseline_t monitor;
baseline_init(&monitor, &config);

// Static allocation - program lifetime  
static baseline_t global_monitor;
baseline_init(&global_monitor, &config);

// Caller-managed heap - explicit lifetime
baseline_t *monitors = malloc(n * sizeof(baseline_t));
for (int i = 0; i < n; i++) {
    baseline_init(&monitors[i], &configs[i]);
}

This separation means the module never allocates. Static analysers can verify memory bounds. Certification auditors can trace memory ownership.

update(): The Single Point of State Change

int baseline_update(baseline_t *ctx, double value, uint64_t timestamp) {
    // Precondition checks
    if (ctx == NULL) return BASELINE_ERR_NULL;
    if (timestamp <= ctx->last_timestamp) return BASELINE_ERR_TEMPORAL;
    if (!isfinite(value)) return BASELINE_ERR_DOMAIN;
    
    // Update timestamp
    ctx->last_timestamp = timestamp;
    
    // State machine transition
    switch (ctx->state) {
        case BASELINE_LEARNING:
            update_learning(ctx, value);
            break;
        case BASELINE_MONITORING:
            update_monitoring(ctx, value);
            break;
        case BASELINE_FAULT:
            // Remain in fault until reset
            break;
    }
    
    return BASELINE_OK;
}

The critical constraint: all state changes happen in update().

This isn’t a stylistic preference. It’s what makes the module analysable:

Determinism: If all state changes occur in update(), then given the same sequence of update() calls with the same inputs, the module will always reach the same state. Execution can be replayed exactly.

Atomicity: Each update() call is a complete state transition. There’s no partial update, no intermediate state visible to other code. Either the update completes or it fails cleanly.

Testability: To test any state, construct the sequence of update() calls that reaches it. No hidden dependencies, no setup that isn’t explicitly part of the test.

Traceability: In safety-critical systems, auditors need to trace how state evolved. With all changes in update(), the trace is the sequence of inputs.

status(): Pure Query Function

baseline_state_t baseline_status(const baseline_t *ctx) {
    if (ctx == NULL) return BASELINE_FAULT;
    return ctx->state;
}

The critical constraint: status() has no side effects.

The const qualifier on the context pointer isn’t optional decoration - it’s a contract enforced by the compiler. status() cannot modify the context, cannot increment counters, cannot update timestamps.

This purity enables:

Concurrent access: Multiple threads can call status() simultaneously without synchronisation. The function only reads.

Idempotent queries: Calling status() twice returns the same result (assuming no intervening update()). There’s no observer effect.

Debug instrumentation: Logging, monitoring, and debugging code can query status() freely without affecting behaviour. The act of observing doesn’t change what’s observed.

Formal verification: Pure functions are easier to reason about mathematically. The output depends only on the input, with no hidden state.

reset(): Return to Known State

void reset(baseline_t *ctx) {
    if (ctx == NULL) return;
    
    // Preserve configuration
    baseline_config_t saved_cfg = ctx->cfg;
    
    // Clear all runtime state
    ctx->state = BASELINE_LEARNING;
    ctx->sample_count = 0;
    ctx->ema = 0.0;
    ctx->variance = 0.0;
    ctx->last_timestamp = 0;
    
    // Restore configuration
    ctx->cfg = saved_cfg;
}

The critical constraint: reset() is always safe to call.

This seems trivial but has profound implications:

Recovery: When a module enters a fault state or behaves unexpectedly, reset() provides a guaranteed escape. The system can recover without restarting.

Testing: Tests can reset between scenarios without reinitialising. State from one test doesn’t leak into the next.

Long-running systems: Systems that run for months need a way to clear accumulated state. reset() provides this without the complexity of full reinitialisation.

Composition: When composing modules, a supervisor can reset any subordinate module at any time. This simplifies error handling at the system level.

Note what reset() preserves: configuration. The module returns to its initial state but remembers how it was configured. This distinction matters - configuration represents design-time choices, while runtime state represents execution history.

Why This Pattern Enables Certification

Safety-critical standards like DO-178C, IEC 62304, and ISO 26262 require properties that the Init-Update-Status-Reset pattern provides by design.

Bounded Memory (MC/DC Coverage)

// Maximum memory usage is sizeof(baseline_t)
// This is known at compile time
// No allocation = no fragmentation = no memory exhaustion

Certification requires proving that software cannot exhaust memory. Dynamic allocation makes this proof difficult or impossible. With caller-provided memory and no internal allocation, the proof is trivial: memory usage equals the sum of context struct sizes.

Bounded Execution Time (WCET)

int baseline_update(baseline_t *ctx, double value, uint64_t timestamp) {
    // Fixed number of operations
    // No loops with data-dependent bounds
    // No recursion
    // WCET can be computed by static analysis
}

Worst-Case Execution Time analysis requires that every code path have a provable upper bound. The pattern enforces this:

  • init(): Fixed sequence of assignments
  • update(): Fixed state machine with bounded transitions
  • status(): Single field access
  • reset(): Fixed sequence of assignments

No function contains unbounded loops, recursion, or data-dependent iteration. WCET tools can analyse each function automatically.

Deterministic Execution

// Same sequence of inputs → same sequence of states
// No hidden state, no external dependencies, no randomness

Reproducibility is fundamental to testing and certification. If a test passes, it must continue to pass. If a failure occurs, it must be reproducible for diagnosis.

The pattern guarantees determinism:

  • init() depends only on configuration (static)
  • update() depends only on context and input (explicit)
  • status() depends only on context (read-only)
  • reset() depends only on context (deterministic)

There are no hidden inputs: no global variables, no system calls, no timestamps from hardware clocks (timestamps are passed as parameters).

Traceability

// Every state is reachable via a sequence of update() calls
// That sequence is the trace
// The trace is the specification for reproduction

When something goes wrong, auditors need to know how the system reached that state. With all state changes in update(), the trace is simply the sequence of (input, timestamp) pairs passed to update().

This trace can be:

  • Logged for post-incident analysis
  • Replayed for debugging
  • Analysed for coverage
  • Verified against requirements

Comparison with Object-Oriented Patterns

Traditional OOP approaches to stateful components look different:

class BaselineMonitor {
public:
    BaselineMonitor(const Config& cfg);
    ~BaselineMonitor();
    
    void processValue(double value);  // Modifies state
    State getState() const;            // Queries state
    double getEMA() const;             // Queries state
    void setThreshold(double t);       // Modifies config AND state?
    
private:
    State state_;
    double ema_;
    // ...
};

This design has several properties that complicate certification:

Constructor allocation: The constructor may allocate memory, making resource bounds harder to prove.

Multiple mutators: Both processValue() and setThreshold() modify state. State changes aren’t localised.

Blurred boundaries: Does setThreshold() just change configuration, or does it also affect runtime state? The interface doesn’t say.

Destructor complexity: Destructors can fail, throw exceptions, or have side effects. Resource cleanup is implicit.

No explicit reset: How do you return the object to initial state? Destroy and reconstruct? That involves allocation.

The Init-Update-Status-Reset pattern makes explicit what OOP often leaves implicit:

ConcernOOP ApproachIUSR Pattern
MemoryConstructor allocatesCaller provides
State changeMultiple methodsOnly update()
ConfigurationMixed with runtimeSeparate in init()
ResetUnclear/reconstructExplicit reset()
AnalysisRequires whole-programPer-function

Pattern Composition

Modules following the pattern compose naturally. Consider a system with temperature and pressure monitors:

typedef struct {
    temp_monitor_t temp;
    pressure_monitor_t pressure;
    system_state_t state;
} system_t;

int system_init(system_t *sys, const system_config_t *cfg) {
    int err;
    
    err = temp_init(&sys->temp, &cfg->temp);
    if (err != TEMP_OK) return SYSTEM_ERR_TEMP_INIT;
    
    err = pressure_init(&sys->pressure, &cfg->pressure);
    if (err != PRESSURE_OK) return SYSTEM_ERR_PRESSURE_INIT;
    
    sys->state = SYSTEM_NOMINAL;
    return SYSTEM_OK;
}

int system_update(system_t *sys, sensor_data_t *data, uint64_t timestamp) {
    int temp_err = temp_update(&sys->temp, data->temperature, timestamp);
    int press_err = pressure_update(&sys->pressure, data->pressure, timestamp);
    
    // Compose component states into system state
    if (temp_err != TEMP_OK || press_err != PRESSURE_OK) {
        sys->state = SYSTEM_SENSOR_FAULT;
    } else if (temp_status(&sys->temp) == TEMP_ALARM || 
               pressure_status(&sys->pressure) == PRESSURE_ALARM) {
        sys->state = SYSTEM_ALARM;
    } else {
        sys->state = SYSTEM_NOMINAL;
    }
    
    return SYSTEM_OK;
}

system_state_t system_status(const system_t *sys) {
    return sys->state;
}

void system_reset(system_t *sys) {
    temp_reset(&sys->temp);
    pressure_reset(&sys->pressure);
    sys->state = SYSTEM_NOMINAL;
}

The system-level module follows the same pattern as its components. This fractal structure means:

  • The system can be tested the same way as components
  • System-level guarantees derive from component guarantees
  • Another system can compose this system as a component

Composition preserves properties. If all components have O(1) memory, the composition has O(1) memory. If all components are deterministic, the composition is deterministic.

Implementing the Pattern

State Machine Structure

Most modules following this pattern contain an explicit finite state machine:

typedef enum {
    STATE_INIT,
    STATE_LEARNING,
    STATE_MONITORING,
    STATE_ALARM,
    STATE_FAULT
} module_state_t;

// Transition table (documentation)
// STATE_INIT + init() → STATE_LEARNING
// STATE_LEARNING + enough_samples → STATE_MONITORING
// STATE_MONITORING + anomaly → STATE_ALARM
// STATE_ALARM + normal → STATE_MONITORING
// ANY + fault → STATE_FAULT
// STATE_FAULT + reset() → STATE_LEARNING

The state machine makes transitions explicit and analysable. Tools can verify that all states are reachable, all transitions are handled, and no invalid transitions occur.

This connects to Heartbeats and State Machines - the same FSM principles apply to any temporal monitoring problem.

Error Handling

The pattern uses return codes rather than exceptions:

typedef enum {
    MODULE_OK           =  0,
    MODULE_ERR_NULL     = -1,
    MODULE_ERR_TEMPORAL = -2,
    MODULE_ERR_DOMAIN   = -3,
    MODULE_ERR_OVERFLOW = -4,
    MODULE_ERR_STATE    = -5,
    MODULE_ERR_CONFIG   = -6
} module_error_t;

Return codes are:

  • Analysable (static analysis can track error propagation)
  • Predictable (no hidden control flow)
  • Explicit (callers must handle them)

The pattern requires that update() returns an error code. Even when the update succeeds, the return value confirms it.

Time Handling

Time is passed as a parameter, not read from system clocks:

int module_update(module_t *ctx, double input, uint64_t timestamp);

This design choice enables:

Deterministic replay: Given recorded inputs and timestamps, execution can be replayed exactly.

Testing: Tests can simulate arbitrary time progressions without mocking system calls.

Portability: No dependency on platform-specific time APIs.

Analysis: Time-dependent behaviour is explicit in the interface.

The module enforces monotonic time internally:

if (timestamp <= ctx->last_timestamp) {
    return MODULE_ERR_TEMPORAL;
}

Out-of-order timestamps are rejected rather than causing undefined behaviour.

When Not to Use This Pattern

The Init-Update-Status-Reset pattern optimises for specific properties. It’s not always the right choice.

Interactive applications: GUI components often need multiple entry points, event handlers, and partial updates. The strict lifecycle is too rigid.

Streaming data: When processing continuous streams with complex dependencies, the single update() entry point may force awkward batching.

Performance-critical inner loops: The precondition checks in update() add overhead. In tight loops where inputs are guaranteed valid, this overhead matters.

Exploratory development: When requirements are unclear, the upfront design required by this pattern can slow iteration.

The pattern shines when:

  • Correctness must be provable
  • Behaviour must be reproducible
  • Resources must be bounded
  • Components must compose reliably
  • Certification is required

For safety-critical systems, these properties aren’t optional. For other systems, they’re often valuable but not mandatory.

Conclusion

The Init-Update-Status-Reset pattern is the minimal interface that provides O(1) resource bounds, deterministic execution, and compositional verification.

The four functions aren’t arbitrary - each serves a specific purpose:

  • init() separates configuration from allocation
  • update() localises all state changes
  • status() guarantees side-effect-free queries
  • reset() provides guaranteed recovery

Together, they enable the properties that safety-critical certification requires: bounded memory, bounded time, deterministic behaviour, and complete traceability.

The c-from-scratch repository demonstrates this pattern across multiple modules. Each module - Pulse, Baseline, Timing - implements the same interface, enabling composition and consistent testing.

The pattern’s constraints feel restrictive at first. All state changes in one function? No internal allocation? Explicit time parameters? These choices force discipline that pays off during testing, debugging, and certification.

As with any architectural pattern, appropriateness depends on requirements. For systems where “it usually works” isn’t acceptable, the Init-Update-Status-Reset pattern provides a foundation for code that provably works.

Four functions. Complete lifecycle. Verifiable contracts.

About the Author

William Murray is a Regenerative Systems Architect with 30 years of UNIX infrastructure experience, specializing in deterministic computing for safety-critical systems. Based in the Scottish Highlands, he operates SpeyTech and maintains several open-source projects including C-Sentinel and c-from-scratch.

Discuss This Perspective

For technical discussions or acquisition inquiries, contact SpeyTech directly.

Get in touch
← Back to Insights