A coordinator often starts as useful glue. It creates a few components, forwards frame updates and exposes convenient callbacks. As the feature grows, it becomes the only place that knows when audio, animation, input, metrics and presentation should start or stop. Each new requirement adds another reference and another ordering rule.

The visible symptom is a large class. The deeper problem is ownership debt: resources live in one component, state is stored in another, updates arrive through a third and shutdown is decided somewhere else. No single boundary can guarantee its own invariants.

1. Identify responsibility that crosses too many hands

Before moving code, trace one live behavior from creation to teardown. Mark:

  • Who creates and disposes the resource.
  • Who stores its current state.
  • Who advances it each frame or reacts to events.
  • Who publishes results to other systems.
  • Who decides that it is safe to stop.

If those answers point to several unrelated objects, choose one responsibility to repair first. Frame dispatch, subscription ownership and state publication are good seams because they expose lifecycle mistakes quickly.

A dependency graph is more useful here than a folder diagram. The question is not where a class lives; it is which direction control and data flow at runtime.

2. Freeze the observable contract before changing structure

Refactoring production behavior needs an external definition of “unchanged.” Capture the consumer-visible facts: state transitions, output values, event ordering, visual result and shutdown behavior. Preserve representative logs or a short repeatable scenario where automated coverage is not practical.

Do not lock in accidental internals. A middleman callback, duplicated cache or public field used only inside the repository is not a product contract. The contract belongs at real boundaries: persisted data, serialized assets, network messages, public APIs and behavior other systems consume.

Refactor targetPreserve what consumers observe. Replace the path that produces it. Keeping both old and new internal paths active usually creates more uncertainty than it removes.

3. Transfer one responsibility to its lifecycle owner

Suppose a scene-level coordinator calls a subsystem’s frame method, copies its result and republishes it. If the subsystem already owns the resource and knows when it is active, it is usually the better owner of its update loop. Move the work there, update every caller and delete the forwarding path in the same change.

The transfer should include the complete responsibility:

  1. Initialization with required dependencies.
  2. Activation and any event subscriptions.
  3. Frame or event processing.
  4. Publication of observable state.
  5. Deactivation and cleanup.

Moving only the method while leaving activation or state elsewhere creates distributed ownership with a different shape. The new owner must be able to explain its own valid states and guarantee its own cleanup.

4. Publish state directly instead of forwarding notifications

Coordinator chains often exist to relay values: subsystem → feature manager → scene manager → consumer. Every relay adds ordering, lifetime and duplication questions. When several consumers need the same runtime fact, expose it from the owner through a focused observable state contract.

Consumers subscribe for their own lifecycle and unsubscribe when they leave. The owner publishes one coherent snapshot rather than a sequence of loosely related callbacks. This reduces temporal coupling: a new consumer no longer requires another forwarding method in the coordinator.

Snapshots should be meaningful and stable enough for consumers, but not become a dump of private implementation details. Publish what another system needs to decide or present, not every intermediate field.

5. Make validation paths pure

Validation becomes dangerous when it quietly creates objects, normalizes serialized data in place or changes runtime state. An authoring tool may appear to “check” an asset while also repairing it, which makes failures depend on inspection order.

Separate three operations:

  • Read: inspect the authored or runtime state without mutation.
  • Validate: return explicit findings from that state.
  • Transform: create a normalized result or apply an intentional repair.

Pure validation gives migrations a trustworthy gate. It also makes editor tooling safer because previewing, selecting or reopening an asset cannot silently change the data being evaluated.

6. Quiesce the runtime before visual teardown

Complex transitions often fade, unload or replace presentation while simulation is still running. During that window, late updates can publish new state, restart audio, move a target or trigger callbacks against objects already leaving the scene.

Use an explicit shutdown sequence:

  1. Stop accepting new commands.
  2. Disable producers and frame-driven work.
  3. Release or cancel asynchronous operations.
  4. Publish the final stable state if consumers require it.
  5. Perform the visual transition and dispose presentation.
  6. Release the remaining session resources.

This “quiesce, then transition” rule is especially valuable when several subsystems react independently. It replaces a collection of timing assumptions with one lifecycle boundary.

7. Delete the obsolete path and prove the real consumer moved

A migration is incomplete when the new component works but the old publisher, ticker or adapter remains available. Search all callers, serialized references and construction paths. Update them together, remove obsolete types and verify that the observable output now originates from the new owner.

Prefer a sequence of vertical responsibility transfers over one repository-wide rewrite. Each step should leave the product in a coherent state and make the next boundary simpler. Useful evidence includes:

  • The same representative flow before and after the transfer.
  • No duplicate callbacks or frame processing.
  • Clean entry, interruption and exit from the feature.
  • No obsolete references in code or serialized content.
  • A smaller coordinator whose remaining responsibilities can be stated clearly.

The goal is not maximum decentralization. It is explicit ownership: orchestration composes lifecycles, components own their resources, and consumers observe stable contracts without depending on the route data once took.

Untangling a Unity subsystem under active delivery?My Unity project rescue work combines architecture cleanup with a verified path back to shipping.