---
id: object-model
class: b2
sourced: 2026-07-12
source: "b2b-ux-patterns ch.1"
license: open
---
# Pass: object model

**Inputs:** for each relationship on the object map — `relationshipCardinality`, `childHasOwnLifecycle`, `childMeaningfulAlone`, `editedAtomicallyWithParent`, `associationHasPayload`; plus `collaboration` and `glanceContextNeeded` for the map as a whole.
**Owns:** `requiredMechanics`, `clarifications`.
**Output artifact:** the charted object map — every relationship with its assigned UI treatment, junctions promoted where they carry payload, composites flagged for atomic editing, the denormalization list, and a birth inventory per object (the birth inventory's per-path UX is the `creation-paths` pass).

## The rule

Cardinality deterministically dictates UI treatment — never information size. Two children of identical byte-size get different components based on multiplicity and independence:

- **1:1** embeds as fields on the parent detail; no separate surface.
- **1:few** (bounded, no own lifecycle) is an inline list, fully visible, add/remove in place.
- **1:many** (unbounded, independently meaningful) is a related list — an embedded mini-index with count, a few columns, row actions, and a "view all" escape to the full filtered index.
- **M:N** gets a picker (search-select plus create-and-link). **Independence overrides upward:** a 1:few child with its own lifecycle (states, owner, history) earns related-list treatment despite being few.

**Junction-as-object:** the moment an M:N association needs any attribute (a role, a label, a date, a status) or its own history, the junction is a first-class object — the picker captures the payload at link time, the related list shows the payload as columns, and editing the association edits the junction, not either endpoint. Pure membership with no payload stays an invisible link table.

**Composite / line-item children** (invoice + line items, form + questions) are meaningless alone and edited atomically with the parent: master-detail *form* treatment — an editable grid inside the parent's create/edit surface, saved as one unit. Giving line items related-tab navigation produces absurd flows (navigate away from an unsaved invoice to create its second line).

**Denormalization is an explicit pass:** for every relationship ask "which fields from the other side must appear here without a click?" — parent identity fields on child rows (lookups), child aggregates on parents (rollups). Skipping it is why v1 tables show bare foreign-key names.

**Users are objects in the model.** There is no separate collaboration layer: ownership, assignment, watching, comments, @mentions, and read/unread are ordinary relationships between User and record. The test of a complete chart: three people touch every record — the model must say who owns it, who watches it, who said what, who has seen it. The User object carries `actor ∈ {human, agent}`; agents are Users, but the fork must be declared because assignment, delegation, audit origin, and access all branch on it.

Consult this file once per relationship decision; if cardinality is not yet known, clarify before assigning any treatment.

```json decision-table
{"pass": "object-model", "rows": [
  {"when": {"relationshipCardinality": "one-to-one"}, "then": {"requiredMechanics": {"mustInclude": ["embed-as-fields"], "mustNotInclude": ["related-list-with-view-all"]}}, "reason": "one-to-one-embeds"},
  {"when": {"relationshipCardinality": "one-to-few", "childHasOwnLifecycle": false}, "then": {"requiredMechanics": {"mustInclude": ["inline-list-in-place"], "mustNotInclude": ["related-list-with-view-all"]}}, "reason": "bounded-children-inline"},
  {"when": {"relationshipCardinality": "one-to-few", "childHasOwnLifecycle": true}, "then": {"requiredMechanics": {"mustInclude": ["related-list-with-view-all"]}}, "reason": "independence-overrides-cardinality"},
  {"when": {"relationshipCardinality": "one-to-many", "childMeaningfulAlone": true}, "then": {"requiredMechanics": {"mustInclude": ["related-list-with-view-all", "quick-create-inline"]}}, "reason": "unbounded-children-related-list"},
  {"when": {"relationshipCardinality": "many-to-many", "associationHasPayload": false}, "then": {"requiredMechanics": {"mustInclude": ["association-picker"], "mustNotInclude": ["junction-first-class"]}}, "reason": "pure-membership-invisible-junction"},
  {"when": {"relationshipCardinality": "many-to-many", "associationHasPayload": true}, "then": {"requiredMechanics": {"mustInclude": ["junction-first-class", "payload-at-link-time", "payload-as-columns"]}}, "reason": "junction-carries-payload"},
  {"when": {"childMeaningfulAlone": false, "editedAtomicallyWithParent": true}, "then": {"requiredMechanics": {"mustInclude": ["master-detail-editable-grid", "atomic-parent-child-save"], "mustNotInclude": ["related-list-with-view-all"]}}, "reason": "line-items-edit-atomically"},
  {"when": {"glanceContextNeeded": true}, "then": {"requiredMechanics": {"mustInclude": ["lookup-fields-on-child-rows", "rollup-aggregates-on-parent"]}}, "reason": "denormalize-for-display"},
  {"when": {"collaboration": "concurrent"}, "then": {"requiredMechanics": {"mustInclude": ["users-as-objects", "ownership-assignment-fields", "watcher-junction", "comment-child-objects", "per-viewer-seen-state"]}}, "reason": "three-people-touch-every-record"},
  {"when": {"relationshipCardinality": "__absent__"}, "then": {"clarifications": ["cardinality-unspecified"]}, "reason": "treatment-needs-cardinality"}
]}
```
