Skip to content

Maintain C++ ECS: Invariants, Change Impact, and Tests

A maintainable design exposes its rules and makes likely mistakes testable. Abstractions help only if the team can preserve the invariants they introduce.

Rules We Already Own

Invariant Enforcement Evidence
Valid dt before mutation Step entry Invalid-step tests
Acceleration needs movement Seed validation Rejected combination
Handle belongs to one world Issuer and resolution Foreign number collision
Deleted entity leaves no data erase_all Five store counts become zero
Decisions see stable membership Deferred wrapper Callback inspection
Retry does not replay prefix Pop successful commands Controlled second-spawn failure
Visibility is not motion Update/query rules Invisible-body regression

Explicit test failures remain enabled in Release. Passing fixtures does not prove every possible execution safe.

Walk a Future Change

Consider Drag scaling velocity before movement. This is an exercise, not an implemented feature.

Records need data, validation, update condition, observation, and tests. Composition must decide whether Drag belongs inside Motion. Inheritance must decide whether it is an algorithm variant or independent configuration.

ECS also needs store construction, rollback, cleanup, pass order, and snapshots. A new pass may clarify its writer while increasing consistency obligations.

Do not count only the new loop.

Test Different Mistakes

  1. Independently calculate a trajectory.
  2. Compare full normalised snapshots.
  3. Exercise invalid input and stale handles.
  4. Check final movement and cleanup.
  5. Reproduce a deliberately wrong condition.
  6. Inject a queue failure and test recovery.

Independent expectations protect against shared helper bugs. Cross-design comparisons protect against accidental semantic differences.

Failure Boundaries Are API Behaviour

/tmp/pi-design-main/design_demo maintenance
/tmp/pi-design-main/design_tests maintenance
invalid acceleration rejected; bodies=1
after cleanup: positions=0 velocities=0 accelerations=0

Rejected input leaves existing bodies unchanged. Consumed but unpublished IDs are acceptable. A queue can leave an applied prefix: recoverable is not transactional.

A caller must choose retry, discard, or stop without inadvertently running simulation again.

Deliberate Limits

No parallel scheduler, generic query API, serialization protocol, or runtime component editing is provided. Owner markers are process-local, not save-file IDs.

Generations and sparse indices impose extra invariants only when their requirements exist. Keep those experiments outside the main course's dependency chain.

Exercise

For a proposed component, name the writer, cleanup path, invalid states, observation policy, and order test before debating container choice.

Next: choose a design for your requirements, not for the final chapter.


Previous: Chapter 8 · Course overview · Next: Chapter 10

Donate