---
title: "System design before code"
slug: /workflows/system-blueprint/
markdown: /workflows/system-blueprint.md
section: project-setup
tier: 1
summary: "Box every new system into a fixed shape (data, runtime, authoring tool) before writing code."
sources: ['system-blueprint.md']
canonical: https://unityworkflowlibrary.dev/workflows/system-blueprint/
read_first: /read-first.md
---

> Read first, on every prompt: [non-destructive rules](/read-first.md).

# System design before code

> Box every new system into a fixed shape (data, runtime, authoring tool) before writing code.

Every other rule in this library is a PROHIBITION. Prohibitions leave infinite design space, so an
agent that obeys all of them still invents a new bad shape each time and the debt returns wearing a
different face. This file is the positive half: the shape is prescribed, and anything that is not
this shape is wrong even when it breaks no named rule.

Two layers, two shapes, one seam between them. Pick by where the state lives, never by convenience.

## The declaration (write it BEFORE the code, in the plan or report)

```
SYSTEM:        <name>
RESPONSIBILITY:<one sentence>
OWNS (writes): <the one piece of state it is the SOLE writer of>
READS:         <what it consumes, and from whom>
PUBLISHES:     <the event / component it raises, and the payload>
BLAST RADIUS:  <the exact subscriber list, and how you enumerated it>
ORDER:         <UpdateInGroup / execution order, or the lifecycle hook>
TEARDOWN:      <the paired removal for every subscription, allocation and registration>
AUTHORING:     <where a human inspects and changes this without an agent and without editing C#>
```

- **If RESPONSIBILITY needs the word "and", it is two systems.** Split it now; splitting later means
  untangling two states that have grown into each other.
- **If BLAST RADIUS cannot be enumerated, STOP — that is a coupling defect, found.** See
  CHAIN-MAP-IS-A-DECOUPLING-AUDIT. Fix the coupling; do not proceed with a
  guess and do not widen the search to compensate.
- **If OWNS names state another system also writes, the design is already wrong.** One writer. Every
  other system reads.
- **TEARDOWN is written at the same time as the registration, never "added later".** Statics survive
  domain reload; a subscription with no paired removal is a leak with a delayed fuse.
- **If AUTHORING is "read the code", the system is not designed yet.** Every feature ships the
  surface a human operates it from, in the same slice as the feature. See Shape C below; "n/a" is a
  legitimate answer only for a system with no numbers, no catalog and no inspectable state, and you
  write *why* rather than leaving the line blank.
- The declaration is cheap (9 lines) and it is where design failures are catchable. After the code
  exists they cost a refactor.

## Shape A — MonoBehaviour / presentation / flow layer

Canonical annotated code: `examples/system-ui-split.md`. That file is the REFERENCE IMPLEMENTATION,
not an illustration — a new manager/controller/view matches it or explains why in the declaration.

```
gameplay system → static event → data system → static event → view → UnityEvent → animation / sfx
```

- **Manager owns exactly one responsibility** and is the sole writer of that state. It publishes what
  happened; it never reads or calls another manager. → the [C# style](/workflows/csharp-style/) page EVENT-ONLY-MANAGER-TOPOLOGY.
- **Emit facts, not other systems' conclusions.** `OnEnemyKilled(enemyId)`, never `OnScoreEarned(10)`
  — points are the score system's business, and the lookup source stays swappable precisely because
  combat never knew it.
- **A controller belongs to one manager** and references only that manager. Everything else it needs
  arrives as an event it subscribed to.
- **The view holds only the refs it directly drives**, subscribes to the system's static events, and
  fans out through designer-wired `UnityEvent`s so animation/audio/other widgets need no C# refs.
- **Every reference is `[SerializeField]`, wired at design time on a prefab.** No scan, no `Find`, no
  `GetComponent`, no caching a scan. → the references page.
- **The static event IS the seam.** A subscriber references the TYPE, never an `Instance`.

## Shape B and the seam (DOTS / ECS)

The DOTS / ECS layer shape (Shape B) and the seam from ECS state to presentation now live in the DOTS & ECS section, on the ECS core mechanics page. Shape A, Shape C and the declaration below apply to every project.

## Shape C — the authoring surface (every feature ships one)



Shapes A and B decide how the system is built. Shape C decides whether anyone but the agent that wrote
it can operate it. A feature reachable only through an agent is a black box that agent built around its
own job, and it is the failure mode this whole library exists to prevent — the debt is not in the code,
it is in the fact that nobody else can move it.

Reference implementation: `examples/designer-console.md`. Mechanics and the tuning-layer split:
the editor tooling page.

- **One host window per project; a feature contributes a TAB, never a window.** The tab is discovered
  by reflection with no registration call, so there is no static list to leak across a domain reload.
  A one-shot utility contributes a tagged static method instead. Two windows over the same asset is
  ONE-PATH-CODE-SHAPE broken with a nicer UI.
- **The tool assembly graph is one-way and compiler-enforced.** Domain editor assemblies reference a
  zero-dependency Contract assembly, never the shell. Tooling can then never cycle back into gameplay,
  and it can never become load-bearing.
- **The tab is a VIEW, never an owner.** It reads and writes through the same asset, event or component
  the game uses. A value that lives only in the tab is a second source. A tab that reaches into a
  system is reporting a missing seam — that is the finding, not the fix.
- **The declaration's AUTHORING line names the surface before the code exists**, for the same reason
  BLAST RADIUS does: retrofitting a tool means first discovering what the system's real inputs were.

## Why this is the box

A prohibition list says what not to do and leaves the shape open. A blueprint fixes the shape, so the
non-deterministic part of the work (naming, algorithm, the actual logic) varies while the structure
does not. Tech debt in this codebase has never come from a clever algorithm; it came from a system
that read something it did not own, so nobody could say what breaking it would affect.

Gates that back this file: `tools/rule-check.sh` (references, names, lookups) and
`tools/one-path-sweep.sh` (second paths, manager topology). Neither can see a wrong SHAPE — that is
what the declaration is for, which is why the declaration is written before the code and not after.

## PLACEHOLDER-DIES-WITH-THE-SHAPE — when the real shape lands, the placeholder is migrated or deleted, never kept (HARD)



A placeholder is authored to unblock work before the model exists. The moment the model exists, that
placeholder is **live data pretending to be a decision**, and every hour spent tuning behaviour
against it is wasted twice — once making the change, once undoing it.

**In the same slice that lands the shape**, every existing instance is either migrated onto it or
deleted. "We will migrate later" means the placeholder becomes the reference the next agent copies.

How placeholders announce themselves — treat each as a defect found, not a curiosity:

- **Instances that are identical where they should differ.** Several bodies/items/factions carrying
  the same physical numbers is not a design; it is one placeholder pasted N times. Nothing in the
  artefact distinguishes "deliberate" from "copy-paste", so the next agent cannot tell either.
- **A field with no implementation behind it.** A count, a flag or an id that nothing reads is a
  promise the data model does not keep. Either build what it describes or delete the field — leaving
  it advertises a capability that does not exist and it WILL be built against.
- **A value authored before the thing it references existed.** A position placed before the terrain
  generator, a reference before the prefab, a threshold before the units settled. It was never
  measured against the real thing, so it is a guess with a timestamp.
- **A derived quantity that is authored by hand.** If a number can be computed from other authored
  numbers, authoring it separately guarantees the two disagree eventually, and the disagreement is
  silent. Derive it and validate it; see the pipeline blueprint page INPUTS.

**Do not build on top of a known placeholder.** Fixing presentation, tuning, or behaviour against
placeholder data produces a fix that is only correct for data you are about to throw away, and it
hides the real defect underneath. When you find one mid-task: say so, land the shape and the
migration first, and only then do the work that was asked. This outranks "do only what was asked" —
the same way FIX-WHAT-YOU-LAND-ON does — because the alternative is delivering work that is already
known to be wrong.

**Record the retirement.** The pipeline document (the pipeline blueprint page) names which stage
produced each instance; when a placeholder is retired, the measured value that replaced it and the
measurement that chose it go in the same commit. A number with no recorded measurement is the next
placeholder.
