Architectural principles
Make the requested change understandable, maintainable, and inexpensive to revise. These are defaults
for architectural decisions, not a mandatory architecture, a score, or an always-on mode.
Read the affected code, its consumers, and relevant project guidance first. Establish the required
behavior, compatibility boundaries, and ranked goals. Distinguish discoverable implementation facts
from human-owned priorities. Apply only the principles relevant to the change; do not reorganize an
unrelated codebase to make it conform. For C#, read the implementation flavor.
Other languages use their own equivalent mechanisms; no particular compiler or tool is required.
Decision defaults
- Reuse what fits; justify what is new. Look for existing capabilities, standard-library or native
features, and suitable installed dependencies before inventing a mechanism. Reuse requires matching
responsibilities and contracts, not merely similar code. Compare the ownership and operational cost
of a new dependency with implementing the needed behavior; neither choice is automatically smaller.
- YAGNI. Introduce the fewest lasting concepts needed for actual requirements. New types, layers,
configuration, and extension points must earn their place. Do not build hypothetical features or
hide complexity in generic wrappers and parameter bags to reduce apparent counts.
- Fewest moving parts. Meet actual requirements with the fewest independent mechanisms, policy
owners, and coordination paths. Consolidate competing ways of doing the same job; do not merely hide
them behind one facade. Preserve distinct responsibilities and lifetimes where they matter: fewer
parts does not mean one giant component, fewer files, or the shortest implementation.
- High cohesion, low coupling. Organize by capability and purpose. Prefer vertical slices with
related behavior and contracts adjacent, rather than global buckets for enums, interfaces, or helpers.
Keep a cohesive unit and its small supporting types together, including in one file when readable.
Separate independently useful types and genuinely shared infrastructure by their named responsibility.
A real dependency or public-contract boundary can justify a dedicated abstractions package.
- Explicit dependencies, outcomes, and invariants. Prefer meaningful inputs, typed outcomes, and
explicit transitions over hidden shared state, contradictory flags, or many optional parameters.
Make illegal states unrepresentable where practical. Use value objects and the type system to enforce
real invariants; validate untrusted inputs at boundaries. Typed events can help without requiring an
event-driven architecture or ceremonial wrappers for every primitive.
- Immutable data by default. Prefer immutable values and explicit state transitions. Localize
necessary mutation to a clear owner with a justified lifecycle or performance need. Check contained
collections and aliases; an immutable-looking outer declaration does not prove immutable contents.
- Narrow contracts and intentional extension. Keep constructs implementation-private and closed
to inheritance unless consumers need a supported extension contract. Prefer composition over
inheritance. Do not change an existing public or overridable contract merely to apply these defaults.
Avoid speculative fallbacks and optional inputs that conceal failed assumptions. A required
compatibility or recovery path needs explicit conditions, outcomes, and verification.
- Readability over cleverness. Prefer obvious control flow and named steps to nested ternaries,
dense chains, and compressed expressions. Optimize for a human understanding and reviewing the unit,
not minimum lines, files, or declarations. Simple expressions need no mechanical expansion.
- Extend-only at compatibility boundaries. Preserve released public API, wire, storage, schema,
and behavioral contracts relied upon by consumers or older versions. Prefer additive, opt-in evolution
with compatibility verification. This does not freeze private implementation. Retirement or a breaking
change requires an agreed migration/versioning decision; local simplicity does not erase obligations.
- Preserve optionality. Consider the cost of changing a decision, recovering data, or replacing a
dependency. A modest, justified cost now can preserve inexpensive future choices without implementing
speculative features. Do not let a convenient abstraction dictate destructive domain behavior.
Event sourcing, soft deletion, and replaceable boundaries are techniques, not universal requirements;
respect the project's data-retention and deletion requirements.
- Preserve guarantees and proof. Do not obtain a smaller implementation by silently removing
required behavior, safety checks, or meaningful verification. Make intended behavior changes explicit.
Compatibility adapters and focused tests may legitimately increase code while reducing overall risk.
Resolve trade-offs and finish
Evaluate user suggestions against the same principles and source evidence as your own proposals.
When a suggestion creates concrete coupling, compatibility risk, or an unreviewable expansion, say
what would change, why that matters, and recommend a narrower or more reversible alternative. Ground
pushback in affected behavior or code, not a principle's name alone; label uncertain consequences as
inferences. Do not invent objections to appear rigorous. Respect an informed user choice within hard
constraints and authorized scope; record the accepted trade-off without repeatedly relitigating it.
For consequential, hard-to-reverse choices or conflicts with project priorities, pause that decision
before committing the design or implementation. Present a short recommendation, viable alternatives,
and the consequences; ask one focused human question. Continue independent, authorized work if useful.
Do not treat the absence of a preferred language feature as a blocker or impose event sourcing,
inheritance bans, or a new framework to comply with these defaults.
Produce the requested design, code, or review, not a separate principles report. Explain only material
new constructs, departures, and unresolved trade-offs in the existing artifact or PR. When work spans
turns, checkpoint those decisions and remaining verification there rather than accumulating new notes.
Finish at the requested scope with verification results and limits; an assessment does not authorize
implementation, and applying these principles does not authorize adjacent refactoring or rollout.
The compatibility and reversibility defaults draw on
extend-only design and
high-optionality programming.
These explanations are background, not additional mandatory runtime reading.
1---2name: architectural-principles3description: Guide design, implementation, and review decisions about new constructs, feature organization, state, contracts, and extension points. Apply maintainability and reversibility defaults within the requested change; not a whole-system assessment or a delivery-planning workflow.4---56# Architectural principles78Make the requested change understandable, maintainable, and inexpensive to revise. These are defaults9for architectural decisions, not a mandatory architecture, a score, or an always-on mode.1011Read the affected code, its consumers, and relevant project guidance first. Establish the required12behavior, compatibility boundaries, and ranked goals. Distinguish discoverable implementation facts13from human-owned priorities. Apply only the principles relevant to the change; do not reorganize an14unrelated codebase to make it conform. For C#, read [the implementation flavor](references/csharp.md).15Other languages use their own equivalent mechanisms; no particular compiler or tool is required.1617## Decision defaults1819- **Reuse what fits; justify what is new.** Look for existing capabilities, standard-library or native20 features, and suitable installed dependencies before inventing a mechanism. Reuse requires matching21 responsibilities and contracts, not merely similar code. Compare the ownership and operational cost22 of a new dependency with implementing the needed behavior; neither choice is automatically smaller.23- **YAGNI.** Introduce the fewest lasting concepts needed for actual requirements. New types, layers,24 configuration, and extension points must earn their place. Do not build hypothetical features or25 hide complexity in generic wrappers and parameter bags to reduce apparent counts.26- **Fewest moving parts.** Meet actual requirements with the fewest independent mechanisms, policy27 owners, and coordination paths. Consolidate competing ways of doing the same job; do not merely hide28 them behind one facade. Preserve distinct responsibilities and lifetimes where they matter: fewer29 parts does not mean one giant component, fewer files, or the shortest implementation.30- **High cohesion, low coupling.** Organize by capability and purpose. Prefer vertical slices with31 related behavior and contracts adjacent, rather than global buckets for enums, interfaces, or helpers.32 Keep a cohesive unit and its small supporting types together, including in one file when readable.33 Separate independently useful types and genuinely shared infrastructure by their named responsibility.34 A real dependency or public-contract boundary can justify a dedicated abstractions package.35- **Explicit dependencies, outcomes, and invariants.** Prefer meaningful inputs, typed outcomes, and36 explicit transitions over hidden shared state, contradictory flags, or many optional parameters.37 Make illegal states unrepresentable where practical. Use value objects and the type system to enforce38 real invariants; validate untrusted inputs at boundaries. Typed events can help without requiring an39 event-driven architecture or ceremonial wrappers for every primitive.40- **Immutable data by default.** Prefer immutable values and explicit state transitions. Localize41 necessary mutation to a clear owner with a justified lifecycle or performance need. Check contained42 collections and aliases; an immutable-looking outer declaration does not prove immutable contents.43- **Narrow contracts and intentional extension.** Keep constructs implementation-private and closed44 to inheritance unless consumers need a supported extension contract. Prefer composition over45 inheritance. Do not change an existing public or overridable contract merely to apply these defaults.46 Avoid speculative fallbacks and optional inputs that conceal failed assumptions. A required47 compatibility or recovery path needs explicit conditions, outcomes, and verification.48- **Readability over cleverness.** Prefer obvious control flow and named steps to nested ternaries,49 dense chains, and compressed expressions. Optimize for a human understanding and reviewing the unit,50 not minimum lines, files, or declarations. Simple expressions need no mechanical expansion.51- **Extend-only at compatibility boundaries.** Preserve released public API, wire, storage, schema,52 and behavioral contracts relied upon by consumers or older versions. Prefer additive, opt-in evolution53 with compatibility verification. This does not freeze private implementation. Retirement or a breaking54 change requires an agreed migration/versioning decision; local simplicity does not erase obligations.55- **Preserve optionality.** Consider the cost of changing a decision, recovering data, or replacing a56 dependency. A modest, justified cost now can preserve inexpensive future choices without implementing57 speculative features. Do not let a convenient abstraction dictate destructive domain behavior.58 Event sourcing, soft deletion, and replaceable boundaries are techniques, not universal requirements;59 respect the project's data-retention and deletion requirements.60- **Preserve guarantees and proof.** Do not obtain a smaller implementation by silently removing61 required behavior, safety checks, or meaningful verification. Make intended behavior changes explicit.62 Compatibility adapters and focused tests may legitimately increase code while reducing overall risk.6364## Resolve trade-offs and finish6566Evaluate user suggestions against the same principles and source evidence as your own proposals.67When a suggestion creates concrete coupling, compatibility risk, or an unreviewable expansion, say68what would change, why that matters, and recommend a narrower or more reversible alternative. Ground69pushback in affected behavior or code, not a principle's name alone; label uncertain consequences as70inferences. Do not invent objections to appear rigorous. Respect an informed user choice within hard71constraints and authorized scope; record the accepted trade-off without repeatedly relitigating it.7273For consequential, hard-to-reverse choices or conflicts with project priorities, pause that decision74before committing the design or implementation. Present a short recommendation, viable alternatives,75and the consequences; ask one focused human question. Continue independent, authorized work if useful.76Do not treat the absence of a preferred language feature as a blocker or impose event sourcing,77inheritance bans, or a new framework to comply with these defaults.7879Produce the requested design, code, or review, not a separate principles report. Explain only material80new constructs, departures, and unresolved trade-offs in the existing artifact or PR. When work spans81turns, checkpoint those decisions and remaining verification there rather than accumulating new notes.82Finish at the requested scope with verification results and limits; an assessment does not authorize83implementation, and applying these principles does not authorize adjacent refactoring or rollout.8485The compatibility and reversibility defaults draw on86[extend-only design](https://aaronstannard.com/extend-only-design/) and87[high-optionality programming](https://petabridge.com/blog/high-optionality-programming-pt1/).88These explanations are background, not additional mandatory runtime reading.