deterministic and predictable programming
rule
- same explicit inputs, dependency results, and starting state -> same result and observable transition. a reader traces why a value exists, why a branch ran, and what happened — without guessing at hidden time, ambient config, randomness, mutable globals, or inferred strings.
- deterministic = the mechanism repeats. predictable = a person or agent can anticipate it from named inputs, boundaries, and state transitions. both are required: repeatable hidden behavior is hard to repair; explainable behavior that changes incidentally is still unsafe.
- never derive meaning by rewriting strings. identifiers, paths, URLs, commands, casing, domain values are structured data with one canonical representation: parse or model at the boundary; explicit mappings for closed transformations.
- determinism does not ban time, random IDs, network calls, or side effects — it isolates them at named boundaries, records results, and passes them into the domain decision explicitly.
why
a replayable decision is diagnosable: same inputs and state -> same result narrows a failure to an input, a dependency result, or a transition — not to hidden time, ambient config, or a forgotten string rewrite. string-derived identifiers multiply the risk: casing and delimiters silently change identity and collide. invariant: every value that changes behavior has one canonical representation and enters the decision through a named boundary.
required approach
- parse URLs, paths, JSON, YAML, SQL, command arguments, Markdown with their purpose-built parsers or builders.
- validate external values with schemas; represent closed domains as named enum-like objects.
- store authored slugs, display labels, business keys exactly as authored.
- typed mapping table when one finite domain maps to another.
- pass current time, generated identifier, feature decision, external response as explicit values when they affect a domain decision.
- sort externally sourced collections with an explicit stable comparison before deriving a visible order, batch, digest, or persisted projection.
- make writes idempotent via an explicit operation key and durable state; never rely on a retry happening only once.
keep uncertainty at the boundary
| source of variation | deterministic boundary | domain input |
|---|---|---|
| current time | clock adapter records one instant | occurred_at |
| generated ID | ID provider produces it once | operation_id |
| random selection | chooser records its chosen member | selected_variant |
| remote request | adapter validates and records the response | typed response or typed failure |
| configuration or flag | configuration owner resolves one named decision | enum-like decision value |
| unordered collection | boundary applies an explicit sort | ordered values |
the domain service receives values, not ambient APIs. an adapter may read a clock or call a provider; a mapper may convert a validated response; the decision itself must not reach outward to rediscover the same fact.
make behavior predictable to a reader
one capability, one visible decision path: name the input, validate it, make the state transition, expose the resulting state. never hide a business decision in a callback, getter, presentation component, generic utility, or implicit framework default.
| reader question | code must make visible |
|---|---|
| what can change this outcome? | validated input, named dependency result, and durable starting state |
| which rule selected this branch? | named enum member, guard, or exhaustive mapper |
| what happens next? | explicit state transition or typed failure |
| can a retry change the answer? | durable operation identity and retry state |
| where can this be diagnosed? | correlation data, structured event, and owning boundary |
select the tool by the input's real structure
| input or change | correct owner | why |
|---|---|---|
| URL, path, command argument, or query string | platform parser or typed builder | delimiters and escaping already have a grammar. |
| JSON, YAML, MDX, SQL, or source syntax | purpose-built parser/schema/AST | string patterns cannot prove structural validity. |
| closed external value to internal value | exhaustive mapping over named members | unmapped and future values fail visibly. |
| authored slug, label, or business key | stored exact authored value | the author, not a casing function, owns identity. |
| final human display text | narrow formatting at the final rendering edge | presentation does not become a new identifier. |
decision process
| tempting shortcut | failure | selected direction |
|---|---|---|
replace a route/path segment | delimiter collisions and encoding rules turn a local patch into a different resource | parse and construct from named path parts. |
| lower-case a label into a key | copy editing silently changes identity and creates collisions | request/store an explicit authored key. |
| split a provider value and infer its fields | an undocumented format change becomes silent data corruption | validate a typed provider contract, then map it. |
| regex-rewrite structured source | a near match can corrupt syntax while appearing successful | use a parser/AST or change the owning structured input. |
| silently normalize malformed input | failure loses its repair boundary | reject with a typed diagnostic and correction action. |
boundary workflow
- identify whether the input has a grammar, a closed value set, or authored human meaning.
- parse/validate once at the outer boundary.
- represent the accepted result with named fields or members.
- map between stable shapes only through an explicit directional mapper.
- construct output through a builder or exact stored value; never reverse engineer it from display copy.
effects and retries
separate the deterministic decision from its effect: decide the intended state transition from explicit values -> persist the operation identity and the transition -> perform the external effect through its adapter and record the outcome. a retry reads the durable operation state and either continues the same operation or stops; it never makes a second ambiguous decision.
| concern | deterministic owner |
|---|---|
| whether an operation is valid | pure domain decision from validated input and state |
| whether it already happened | durable operation record keyed by an explicit ID |
| what an external provider returned | validated adapter result with correlation data |
| how retry proceeds | named state transition and idempotency key |
| what a user sees | projection of the durable state, never a guessed success |
good and bad
bad
function createInvoice(customerId: string) {return invoiceRepository.insert({id: crypto.randomUUID(),customerId,issuedAt: new Date(),position: Math.floor(Math.random() * 10),});}
the result cannot be replayed or explained from the input. a retry creates a
different invoice identity, time, and position — a transient network failure
after a successful insert delivers an undetectable duplicate. a test cannot
assert the decision without mocking crypto, the clock, and Math.random
together, so behavior is verified by luck. a disputed queue position cannot be
reconstructed because the choice was never a recorded input.
good
interface DecideInvoiceProps {invoiceId: string;customerId: string;issuedAt: Date;position: number;}function decideInvoice(props: DecideInvoiceProps) {return {id: props.invoiceId,customerId: props.customerId,issuedAt: props.issuedAt,position: props.position,};}
the adapter creates the ID, reads the clock, and chooses the position once. the decision receives those recorded values and always returns the same invoice for the same input.
permitted narrow exception
small final-display formatting is allowed when it cannot become an identifier, route, key, query, or structured value and its output is never read back as input. keep it next to rendering, name it as presentation. once the result crosses a boundary, it is no longer a formatting exception.
do not
- call
replace,split,join, slicing, regex rewriting, or casing methods to derive an identifier, route, storage key, schema name, import path, or domain value. - convert labels into slugs, derive filenames from display copy, or rely on case transformation as a naming convention.
- repair malformed structured input with a best-effort rewrite; reject it with an actionable diagnostic.
- call
Date.now, random APIs, environment reads, or network clients inside a domain decision when their result changes behavior. - depend on incidental collection order, a mutable module singleton, or an implicit retry count for correctness.
- recompute an operation decision on retry after its first result was already recorded.
verification
- review every introduced transformation: parser, builder, schema, or explicit exhaustive mapping.
- search the focused change for string rewrite APIs; justify any final-display formatting explicitly.
- identify every read of time, random state, environment, network, or shared mutable state; verify it enters through one named boundary.
- replay the same input, dependency results, and starting durable state; expect the same decision and idempotent effect behavior.