Architecting a Multi-Engine Monitoring System That Doesn’t Rot

In short: A monitoring system that’s hard to extend ends up frozen: every new tag or capability risks breaking what already works. One strict rule is what lets new engines be added without touching the ones already running.

A monitoring system that starts as one script watching one tag rarely stays that small. Once it proves useful, the natural next request is “can it also do this for these other twenty tags” and, not long after, “can it also flag anomalies, not just report capability.” The architecture that survives that growth looks very different from the architecture that was fastest to write on day one.

The core rule: config never crosses into core logic

The single rule that mattered most: nothing inside the engine layer, the code that actually computes Cp/Cpk, EWMA scores or influence rankings, is allowed to import configuration directly. Every setting a calculation needs (spec limits, smoothing constants, tag lists, thresholds) flows in as an explicit function parameter.

This sounds like ordinary advice (“don’t hardcode configuration”), but the sharper version of it is: don’t let configuration cross the boundary into the code that does the actual computation, even implicitly via import. An import is just as much a coupling point as a hardcoded value; it’s just less visible in a code review. A function that imports a config module can still be accidentally coupled to one specific tag’s settings while looking generic on the surface.

Global defaults with per-tag overrides

Not every tag needs bespoke configuration. Most inherit sensible defaults, such as a standard EWMA smoothing constant or a standard minimum sample size, and only override the handful of settings that genuinely differ (spec limits, obviously, vary per tag). Structuring configuration as defaults plus overrides, rather than a full config block duplicated per tag, keeps the common case simple and the exceptions explicit.

One generic runtime, not one script per tag

Instead of writing a new script for every tag being monitored, there’s a single generic runtime that accepts a config object describing which tag to watch, which engines to run against it, and what its overrides are. Adding a new tag to the system means adding a new config entry, not writing new code. A single entry point launches all of these runtimes concurrently.

Why this made a third engine cheap

When influence scoring was added as a third engine, well after SPC and anomaly detection were already running in production, it plugged into the same generic runtime pattern with zero changes required to the first two engines. That’s the actual payoff of the config boundary: the SPC and anomaly detection code didn’t need to be touched, tested, or even re-read to add something new alongside them.

Making it visible

This part of the system isn’t visible in a chart. It’s visible in whether each engine’s health can be checked independently. A status tile per engine, each bound to its own runtime’s heartbeat and driven by a simple tag-quality and status expression, tells an operator or engineer immediately which engine, if any, has stopped producing fresh scores. Without this, “the architecture is decoupled” is just a code-review claim; with it, decoupling becomes something you can observe operationally, not just something you trust because someone said so.

The broader lesson

“Don’t hardcode configuration” is common advice. The sharper version, learned here, is: don’t let configuration cross the boundary into the code that does the actual computation, even implicitly via import. Keeping that boundary strict is what turns “add a new capability” from a multi-file refactor into a single new file. It’s also what let onboarding a new engineer mean pointing them at one config pattern, not explaining a growing pile of special cases.

This is Part 3 of a 7-part series on building a real-time process monitoring system. Part 4 covers moving this architecture from batch CSV processing to live, real-time PLC data.