Keep a timer local when one controller owns its changes and expiry actions. Use a shared service when independent modules need the same timing state or scheduling interface, with serialized access and explicit cancellation rules. Read-only countdown views do not require shared ownership.
Keep a timer inside the controller when that controller alone starts it, cancels it and decides what happens at expiry. Use a shared timer service when independent modules genuinely need coordinated access to the same timing state. Several screens showing a countdown do not, by themselves, justify shared ownership. The useful distinction is not how many places display a number, but how many modules need authoritative timing information and permission to change it.
Compare ownership before implementation
A local timer is state held by one controller and accessed through that controller's execution path. A shared timer service holds timing state behind an interface used by multiple modules. Both can use the same underlying clock. Moving the timer into another module does not inherently make it more accurate.
A reusable timer function is also not necessarily a shared timer. Several controllers can call the same function while each retains a separate timer instance. That shares code without sharing state.
| Decision point | Controller-local timer | Shared timer service |
|---|---|---|
| Timing state | Stored with the controller's sequence state | Stored behind a service interface |
| Start, reset and cancel | Handled within one controller's execution path | Accepted through a defined command interface |
| Authoritative consumers | Usually one controller | Several modules may need the same timing record |
| Expiry action | Decided directly by the controller | Delivered to a named action owner |
| Read-only countdown views | Receive published controller status | Receive service or controller status |
| Coordination cost | Fewer message paths and lifecycle rules | Requires ordering, identifiers and delivery rules |
| Main architectural risk | Timer access gradually spreads outside its owner | A central service becomes a place where unrelated sequence logic accumulates |
Count authoritative consumers separately from observers. A module that must decide whether a deadline has passed is an authoritative consumer. A view that displays the controller's reported remaining time is an observer.
Map the timing requirement to an owner
Before choosing an implementation, write down the timer's purpose in a short ownership record:
- Start condition: the event or state transition that begins the interval.
- Change authority: the module allowed to restart, extend or cancel it.
- Expiry meaning: the condition that becomes true when the interval ends.
- Action owner: the module that decides the resulting sequence transition.
- Observers: the modules that only need progress or remaining time.
- Lifetime: whether leaving the associated sequence state cancels the timer.
For example, a production controller might wait for a process acknowledgement. The controller starts the wait, handles an acknowledgement, cancels the wait when the sequence changes and decides what a timeout means. A status view shows the remaining interval.
That arrangement still has one timer owner. Publishing status to the view does not require exposing the timer's reset operation.
Now consider an interval that several independent modules must reference as the same deadline. If those modules cannot reasonably obtain the information through the existing controller interface, a shared service may provide a clearer boundary. It should still identify which module owns each expiry action.
Use this comparison for ordinary sequence and status timing. Safety-related timing requires an appropriate safety design and hazard assessment, not just a choice between these software structures.
Where a local timer wins
A local timer fits when timing is part of one controller's state machine. Keeping the state together makes transitions easier to reason about: entering a state can start a wait, leaving it can cancel the wait, and expiry can request a transition through the same decision path.
The timer state normally needs an active flag and either a start time plus duration or a deadline. Use a monotonic time source for elapsed intervals, following the platform's documented handling of range and wraparound. Calendar time is useful for records, but clock corrections should not unexpectedly shorten or lengthen a sequence wait.
Local ownership does not mean an unrestricted variable that other loops can modify. Keep changes behind the controller's interface. An external request to cancel an operation can go to the controller, which decides whether cancellation is valid in its current state.
A local timer is particularly useful when:
- Every expiry action belongs to the same controller.
- Starting and cancelling follow that controller's state transitions.
- Other modules only need reported status.
- The timer has no independent lifetime outside the operation it serves.
Do not add a service merely because another sequence uses similar timing logic. Separate timer instances can use a common helper while retaining independent ownership.
Where a shared timer wins
A shared timer service becomes useful when timing state has a genuine cross-module contract. Examples include several modules referencing one deadline, or a subsystem accepting timing requests from independent controllers through one scheduling interface.
The important feature is serialized ownership, not global visibility. A service should process changes through a defined ordering mechanism rather than let callers write directly into shared timer data.
Its interface needs clear answers to practical questions:
- Does starting an active timer replace the current interval or reject the request?
- Does reset restart the full duration or change an existing deadline?
- Who may cancel a timer?
- How does a caller distinguish its timer from another caller's timer?
- What happens to an expiry notification if its recipient is unavailable?
- What state is reported after cancellation or expiry?
An identifier should distinguish timer records. An activation identifier or generation number should distinguish successive uses of the same record. Those are different jobs: one identifies the timer, while the other identifies the particular interval currently in force.
The service can report that an interval has elapsed without deciding what the machine sequence should do next. Keeping those responsibilities separate prevents a timing utility from becoming a second controller.
Keep expiry actions with the sequence owner
An expiry event is information about timing. It is not automatically permission to perform an action.
Suppose an operation starts a wait, completes early and then starts a new wait using the same timer identifier. An expiry notification from the earlier activation might already be queued. Without an activation identifier, the controller could mistake that old notification for expiry of the new wait.
The action owner should check both the activation identifier and its current sequence state before acting. A stale notification can then be rejected without disturbing the current interval.
Cancellation also needs an explicit contract. If expiry and cancellation arrive close together, their outcome depends on the ordering rules at the timer owner and the state rules at the controller. Do not let whichever callback happens to run first become an undocumented policy.
A useful default is to route expiry through the controller's normal event handling rather than change sequence outputs from a timer callback. This keeps the decision alongside acknowledgements, abort requests and other events that can make a timeout irrelevant.
Separate timing from display updates
A countdown display can refresh less frequently than the controller evaluates its deadline. Its refresh rate does not define the control interval, and a delayed display update does not prove the underlying timer expired late.
Publish enough status to make the view meaningful: the operation state, whether timing is active, remaining duration and the age of that status. A view should distinguish inactive timing from missing or stale information.
If the controller and view share a monotonic clock domain, a timestamped status record can support a locally calculated countdown between updates. Across separate devices, do not assume raw monotonic timestamps are comparable. Define how remaining duration and message age will be interpreted instead.
The boundary between a factory status display and an operator HMI matters here. Showing timing progress is not the same responsibility as accepting commands that change the sequence. Adding a countdown view should not silently give that view authority to reset the controller's timer.
Check timing and lifecycle behaviour
Neither architecture removes scheduling delay. A controller that checks a deadline once per loop can only detect expiry when it next runs. A shared service adds its own execution and message-delivery paths. Decide the acceptable response delay before choosing either structure.
Avoid a long blocking wait if the same execution path must also handle cancellation or incoming events. Use the platform's supported non-blocking timing or event mechanisms so the owner can continue processing relevant work.
Check the design with cases that expose ownership mistakes:
- Start a timer, then cancel it before expiry.
- Restart it while an earlier expiry notification is pending.
- Leave the sequence state before the deadline.
- Process an acknowledgement and expiry close together.
- Delay the consuming loop or message delivery.
- Shut down and restart the timer owner.
- Interrupt status updates while the controller continues operating.
Define the expected result for each case before running the checks. In particular, restart behaviour must be deliberate. An interrupted interval might be abandoned, resumed from valid persisted information or sent to a recovery state. Do not infer that policy from whichever timer fields happen to survive.
Make the decision from the contract
Choose a local timer when one controller owns the interval and all resulting actions. Publish read-only status for observers. This keeps sequence state, cancellation and expiry decisions together without introducing another lifecycle to manage.
Choose a shared service when independent modules require a common timing record or a scheduling interface that cannot sensibly belong to one controller. Give the service serialized access, clear command semantics and activation identifiers. Leave each expiry action with a named sequence owner.
If the only argument for sharing is that several modules need similar timer code, share the implementation rather than the state. If the only argument for keeping it local is avoiding an interface, check whether outside modules are already reaching in to change it. The better architecture is the one whose ownership rules match the actual requirements.
Questions
Does more than one timer reader require a shared timer?
No. Several modules can observe status published by one controller without owning the timer. Consider a shared service when independent modules need authoritative access to the same timing record or a common scheduling interface. Multiple readers alone do not justify multiple writers.
Is a shared timer more accurate than a local timer?
Not inherently. Accuracy and response delay depend on the clock source, execution scheduling, deadline checks and event delivery. A shared service can improve consistency of timing rules, but it also introduces communication paths. Choose the structure by ownership, then check it against the required response delay.
Which module should act when a shared timer expires?
The module that owns the sequence should decide what expiry means in its current state. A shared timer service can issue a notification, but the recipient should check the activation identifier and confirm that the event still applies before requesting a transition.
Can several controllers reuse timer code without sharing a timer?
Yes. A common timer function or class can provide separate instances, with each controller owning its own timing state. That avoids duplicated implementation without creating shared mutable state or requiring unrelated sequences to use a central service.
Planning information, not legal or engineering advice. Rules differ by place: check codes and permits with your local authority and your licensed trades.