9  Doctrine: Principles, Decision Guides, and Named Anti-Patterns

This document compresses the preceding argument into principles, decision guides, and named anti-patterns for design reviews. Each principle is framed by its downstream payoff: knowledge belongs in the model when it removes interpretation, checking, or branching from consumers.

A register note before the principles: everything in this document is doctrine — deliberately imperative defaults distilled from documented S7 behavior and observed ecosystem practice, but laws of neither. The imperative mood is a choice: a crisp default you can overrule for stated reasons is more useful in a design session than a hedge you can ignore. “Always” and “never” here mean “strong default,” and when a principle collides with a real constraint, the earlier documents carry the reasoning needed to make the trade explicitly.

9.1 Principles

Model meaning through representative designs. Begin with the domain, intended consumers, and a few representative values and operations. Record the semantic atoms and laws those examples expose, then revisit both structure and behavior. Meaning can be more stable than document shape, but it is discovered iteratively rather than by completing a universal foundation first.

Let construction retain evidence. Boundary functions turn loose input into representations that record what was learned, so downstream code can demand those representations instead of re-checking raw contents. Construction certifies only invariants visible at that boundary and preserved by supported transformations; references inside an object and external facts remain outside that certificate.

Constructors normalize; validators verify; boundaries translate. Coercion, canonicalization, and version gating happen once, at the boundary. Validators judge canonical values only, and stay pure and cheap because they run on every property write. A validator accommodating multiple input shapes is a parser in the wrong place, running forever.

Give every invariant one authoritative home. Place each law at the lowest boundary that sees everything it mentions: external conformance at the wire, parsing at the boundary, type on the property class, local law in the property validator, cross-property law in the class validator, cross-object law in the aggregate root, operation legality in the mutator, developer invariants in assertions. Each rung assumes the rungs below and duplicates none. A cheap duplicate deliberately retained for better boundary diagnostics is a documented trade; a law found on two rungs by accident is a bug waiting to be two different laws. The governing test is the rule’s visibility horizon. A channel range is visible to one property; start-before-end needs the whole object; unique ids need the collection; authorization needs the live authority. Below that horizon the rule is uncheckable, while above it the rule runs too often and couples code to context it does not need.

Treat presence and default ownership as a contract. Required in the result, caller-supplied, nullable, NA-able, empty, defaulted, and omitted on serialization are separate decisions. Record whether R, an external authority, or the caller requesting materialization owns a default. One optional flag cannot preserve these meanings.

Treat constants as identity when instances cannot differ. If two instances of a class can never differ on a field, the field is part of what the class is: model it as a computed property or inject it at serialization, and keep it out of the constructor. Store it only when round-trip fidelity or spec evolution demands that instances carry it.

Represent alternatives as alternatives. Flag fields, mutually exclusive nullables, and mode strings are sum types flattened into products. Give variants honest representation: concrete variant classes when identity, fields, or behavior differ; enum properties when only the tag differs; unions when one slot accepts several primitive or domain classes. Abstract parents provide shared schema or protocol but do not seal a family. Loud fallbacks and coverage tests detect unsupported known cases; they do not prove exhaustiveness.

Inherit for dispatch and substitutability; compose for structure. Parents contribute a property schema and a dispatch position to things that genuinely are the parent. Everything else — nesting, reuse, independent dimensions — is composition. Hierarchies stay shallow: abstract root, concrete leaves, intermediate levels only when they add a genuine is-a concept and either a useful dispatch position, shared invariant-bearing schema, or structural exclusion of illegal states. Inherit from a base type only when the object is that primitive.

Co-design nouns and verbs. Behavior belongs to generics, and interfaces are generic sets plus laws. Design the verb vocabulary as deliberately as the class graph. An extension point is a set of generics an implementor must support and the laws those methods must satisfy — write the laws down, executably where possible. Let operations revise the representation when they reveal missing distinctions. Reach for multiple dispatch when behavior genuinely varies along two type axes.

Choose lifecycle policy explicitly. Editable values are revised through replacement. Read-only snapshots are values whose public API forbids revision. Reference owners coordinate shared identity, resources, or observable lifecycles. Keep domain knowledge in values where possible, and document any reference-valued payload because outer validation will not observe its internal mutation.

These policies answer different questions. A value says that revision produces another value. A snapshot says that the represented fact is complete and the public API offers no revision, even though S7 replacement machinery still exists underneath. An owner says that aliases must observe one evolving process or resource. A chat may therefore own a live stream while emitting completed turns as snapshots; the hybrid is explicit rather than an exception hidden inside a nominally immutable object.

Revise related properties together, through the front door. Invariants spanning several properties imply every update of those properties goes through one grouped replacement (S7::set_props()), wrapped in a transformation function. Guarantees hold for supported clients of the exported surface; make the front door the easiest path.

Use one authoritative meaning with deliberate projections. When several outputs encode the same knowledge — an expression and a legend; a class graph and its documentation; a wire format and a pretty printer — designate one canonical source and derive deliberate projections. This removes independently maintained definitions and makes consistency testable; translators and generators can still be wrong.

Keep property access cheap and pure. Getters compute derivations; they never perform I/O, never mutate, never surprise. The moment x@thing can be slow or effectful, the object stops being data in the reader’s mind, and the whole value-semantics reasoning collapses.

Make generalized machinery earn its keep. Every layer of indirection pays rent by absorbing a demonstrated axis of variation. Apply the rule of three to factories, metamodels, and framework layers, not to naming an important domain concept. Delete machinery that no longer simplifies its consumers.

Choose schema authority before generation strategy. Validate against external schemas with the schema ecosystem’s own tools at the wire; read foreign schemas as a census that can inform a hand-designed semantic model. Generating a native transport facade may be appropriate when foreign flatness is authoritative. When the package owns a semantic specification, generating classes, validators, schemas, and documentation from it may also be appropriate. Do not confuse either case with mirroring incidental foreign structure without a consumer-driven reason.

The key distinction is between a transport facade and a semantic model. A facade may intentionally preserve foreign names, optionality, and nesting because exact interchange is its job. A semantic model collapses structural duplicates, names domain distinctions, and serves native operations. Some packages need both, joined by an explicit parser or adapter; generation is then a maintenance technique, not evidence that the two layers mean the same thing.

Refuse to guess. Where no correct behavior is known, an error beats a default. sqlr states the reason perfectly in declining to ship an “ANSI SQL” fallback dialect: an untestable default “would quietly accumulate behaviour no database had ever accepted.” The same logic condemns silent coercion of ambiguous input, fallback branches for “unknown type,” and any code path no test can exercise against a real authority. A loud, early error is information; a guess is deferred corruption.

Check as early as the inputs allow, as late as the context requires. The value of a check is proportional to the cost of the work it gates. Declarations should validate themselves at package load (a default violating its own bounds is the author’s bug, catchable before any user exists); configurations should validate fully before the expensive run they configure; and laws that mention context not yet in hand — data dimensions, live resources — should be declared at construction and checked in one named pass at the moment the context arrives, never guessed at and never skipped.

Design failure representation and composition separately. Conditions suit contract violations and control flow that should stop. Computational result values suit expected success or failure that callers compose as data. Domain outcome families describe events such as denied authority, cancellation, or completion. For each subsystem, separately specify whether failures short-circuit or accumulate, which diagnostics survive, and where conditions become values or values become conditions.

A computational result answers “did this calculation produce a value?” and usually carries mechanics such as diagnostics or partial results. A domain outcome answers “what happened in the modeled world?” and may represent denial or cancellation even when the computation behaved perfectly. Keeping those meanings apart prevents a business event from being mistaken for a bug, or an implementation failure from becoming a permanent domain variant.

9.2 Decision guides

The principles explain the defaults; the guides turn them into ordered questions for a concrete design review.

Formulated as the questions to ask, in order, rather than flowcharts to follow mechanically.

Which object system? Is it fundamentally a homogeneous vector that must slice and live in data frames? — vctrs. Does it have identity and a lifetime, with observers sharing its changes? — R6 or an environment. Is it a domain value with laws and dispatchable behavior? — S7. Is it a veneer for ecosystem politeness? — S3. Mixed answers mean a hybrid, which is normal.

Which lifecycle policy? Is ordinary revision through replacement part of the concept? — editable value. Is it a completed fact whose public API should forbid revision? — read-only snapshot. Must aliases observe the same evolving resource or process? — reference owner. A value holding a reference is a hybrid, not a deeply isolated value.

Which sum-type encoding? Do variants differ in fields, or does behavior dispatch on them? — subclass family under an abstract parent. Do they differ only in the tag? — enum-valued property. Is it several primitive shapes or domain classes lawful in one slot? — union. Is the wire genuinely one bag with conditional fields, variants are many and trivial, and dispatch is never needed? — a discriminator plus class validator, chosen consciously as denormalization.

Which semantic-type tier? Does the concept merely repeat a validation shape across sites? — a reused property definition. Must it be impossible to confuse with its base primitive, or does dispatch need it, while it remains truly a primitive? — a class inheriting the base type. Does it have parts, or operations of its own? — a composite class.

Stored field, identity, or boundary-normalized? Can two instances of the class differ on it? If no — identity (computed property or serializer-injected). If yes — must round-trips preserve the caller’s exact value? If yes — stored, with lenient parsing at the boundary and one canonical stored form. Might the spec extend the value set? — stored enum with a parse-time version gate.

Where does a rule belong? Does it mention one property’s intrinsic shape or value? — property contract. Several properties of one object? — class validator. References among contained objects? — aggregate root. External data, metadata, resources, or authority? — contextual operation.

Condition, computational result, or domain outcome? Is the event a contract violation that should unwind control flow? — condition. Is expected failure composed with a computation? — result value plus an explicit short-circuit or accumulation protocol. Is the event meaningful in the domain regardless of computation mechanics? — domain outcome family. Design adapters among channels at named boundaries.

Faithful conversion, adapter, compiler, or intermediate representation? Does the target preserve the same meaning pairwise? — S7::convert(). Is the translation lossy, contextual, one-to-many, policy-bearing, or diagnostic? — a named adapter or compiler. Have multiple real inputs or outputs exposed a stable shared semantics? — consider an intermediate representation then, not before.

How deep should an embedded language go? Does anything analyze or transform the expressions, or target more than one output format? If no — opaque passthrough, with the engine’s validator as judge. Do humans hand-write the expressions painfully? — constructor combinators (shallow embedding). Is analysis, optimization, or multi-target translation the product? — typed AST with generic passes (deep embedding). Representation as deep as the analysis, and no deeper.

9.3 Anti-pattern catalog

The guides identify a choice before implementation. The names below identify the recurring failure shapes once code or a design proposal exists.

Named, so design reviews can point at them.

Schema cosplay. Mirroring incidental foreign structure as nominal domain meaning without identifying the consumers or guarantees it serves. A generated transport facade can be legitimate when flatness or exact fields are authoritative; field count alone proves nothing. Tell: every foreign interface becomes a hand-maintained class, yet downstream code still interprets raw tags and nullable combinations.

Validator soup. The same law independently defined in the constructor, property validator, class validator, and boundary schema. One authoritative declaration may have several deliberate enforcements or projections; the failure is duplicated authority with no agreement tests. Tell: fixing one rule requires unrelated edits in several places.

Shotgun parsing. Input checks interleaved with processing across many functions, no single point after which input is known-good, rejection possible after side effects. Tell: is.null() and is.character() guards deep in core logic; functions that “defensively” re-check their own package’s objects.

Boolean blindness. Check functions returning TRUE/FALSE whose callers immediately need the why or the what — so they re-derive it. Tell: if (is_valid(x)) { ... } followed by code that re-inspects x for the very facts validity established. Return parsed evidence, not verdicts.

The God validator. One class validator enforcing property-local rules, cross-property rules, and collection rules in a hundred lines. Placement discipline lost; every rung’s law in one place, none assumable by the others. Tell: a class validator that re-checks types.

Convert-everything. Routing lossy, contextual, one-to-many translation through a coercion generic because it exists. Coercion generics (S7::convert(from, to)) suit faithful pairwise conversion; translation with diagnostics, options, and asset generation deserves named functions with room for a context argument and a report. Tell: a convert method with side effects or a dozen options.

Expression-system envy. Re-implementing an engine’s expression type system in property validators — badly, partially, and forever chasing the spec. The engine’s validator owns rung 0. Tell: a validator that walks nested lists interpreting operator names.

Decorative hierarchy. Inheritance introduced to share field names or to “organize” classes that never dispatch differently. Dispatch will eventually take the fake is-a seriously. Tell: an abstract parent with one child, or parents whose children never appear in a method signature.

The accidental reference owner. Domain facts held in R6 “for convenience,” creating shared identity where observers did not need it. Tell: $clone() calls sprinkled defensively and lifecycle tests for what consumers treat as a value.

Nullable confusion. NULL doing triple duty as “required but not yet supplied,” “explicitly cleared,” and “omitted on the wire,” with no presence policy. Tell: code that cannot distinguish an untouched field from a cleared one; serializers with ad-hoc per-field if (!is.null()) decisions.

Premature DSL. Combinators, quotation, and clever printing built before any user wrote the target structures by hand long enough to reveal which constructions are actually painful. The DSL then ossifies the wrong abstractions. Tell: a DSL whose vocabulary mirrors the wire format one-to-one instead of the domain’s own idioms.

Effectful getter. A property that reads a file, calls an API, or mutates a cache. Data access becomes action at a distance; cost and effects become invisible at every @. Tell: property access inside a loop dominating a profile; getters needing error handling.

Boundary leakage. Wire names, omit-empty decisions, or dialect quirks appearing in core logic instead of the one serializer pair. Tell: "source-layer" (the wire spelling) as a string anywhere except to_wire()/from_wire().

9.4 A closing note on judgment

The principles compress experience, but they trade against each other in real designs: parse-don’t-validate argues for strictness at the boundary while round-trip fidelity argues for permissive pass-through nodes; earned abstraction argues for waiting while semantic atoms argue for naming meanings early; shallow hierarchies argue against intermediate classes while shared schema sometimes argues for them. The earlier documents exist so that when two principles collide, the reasons behind each are available and the trade can be made explicitly, on the record, instead of by whichever slogan was remembered first.