import { GLOSSARY_SECTION_ICON, MAP_SECTION_ICON } from "#configuration/icons/architecture.icons"; import { GLOSSARY_SECTION_ID, MAP_SECTION_ID } from "#core/ids/architecture.ids"; import { LEX_FACE, TENSION_FACE } from "@govlab/constants"; import type { GlossaryEntry } from "#types/block.types"; import type { Section } from "#types/document.types"; import { VOCABULARY } from "#assets/vocabulary.generated"; import { faceLink } from "#domain/converters/ontology.converter"; import { joinGlossary } from "#domain/converters/link.converter"; const STRUCTURAL = "Structural Core"; const COMPUTATION = "Computation Core"; const RESOURCE = "Resource Core"; const EXECUTION = "Execution Core"; const HUMAN = "Human Factors"; const EVOLUTION = "Evolution Principles"; const PERFORMANCE = "Performance Core"; const PATTERNS = "Design Patterns Core"; const ARCHITECTURE_DIAGRAM = 'flowchart TB\n subgraph core["The four core layers"]\n direction TB\n computation["COMPUTATION · stateless · data flows through, outputs are frozen"]\n resource["RESOURCE · stateful · owned, tracked, explicitly released"]\n execution["EXECUTION · how computation and resources interact"]\n structural["STRUCTURAL · applies to all code · observes itself"]\n end\n human["HUMAN FACTORS · cognitive and discipline constraints"]\n evolution["EVOLUTION · change over time"]\n resource -- observe --> computation\n computation --> execution\n resource --> execution\n execution -- feeds --> structural\n structural -- feedback --> execution\n structural --> human\n structural --> evolution'; const STATE_TENSION_DIAGRAM = 'flowchart LR\n subgraph stateless["COMPUTATION · stateless"]\n direction TB\n s1["Statelessness"]\n s2["Immutability"]\n s3["Explicit Invalidity"]\n s4["Deliberate Under-Specification"]\n s5["Homoiconicity"]\n end\n subgraph stateful["RESOURCE · stateful"]\n direction TB\n r1["State-Before-Mutation"]\n r2["State Over Code"]\n r3["Fail Fast"]\n r4["YAGNI"]\n r5["Single Owner"]\n end\n s1 -- data flows through · a resource is snapshotted before it changes --- r1\n s2 -- computed data is frozen · resource state is mutable but managed --- r2\n s3 -- a computation uncertainty is marked · a resource invariant halts --- r3\n s4 -- interfaces stay open · implementation is not built ahead --- r4\n s5 -- definitions are shared freely · runtime resources have one owner --- r5'; const BOUNDARY_TENSION_DIAGRAM = 'flowchart LR\n subgraph inside["INSIDE ONE BOUNDARY"]\n direction TB\n i1["Consistency"]\n i2["Asynchronous Communication"]\n i3["Canonical Model"]\n i4["Normalisation"]\n i5["Do Not Repeat Yourself"]\n end\n subgraph across["ACROSS BOUNDARIES"]\n direction TB\n a1["Availability"]\n a2["Immediate consistency"]\n a3["Bounded context autonomy"]\n a4["Query performance"]\n a5["Locality of behaviour"]\n end\n i1 -- traded · strong inside a transaction, eventual across autonomy boundaries --- a1\n i2 -- traded · synchronous inside a trust boundary, asynchronous across autonomy boundaries --- a2\n i3 -- traded · one model where contexts share meaning, a translation where they do not --- a3\n i4 -- by scope · the canonical store is normalised, derived read models are denormalised --- a4\n i5 -- by rule · semantics are centralised, incidental co-occurrence stays local --- a5'; const CORE_ENTRIES: readonly GlossaryEntry[] = [ { code: "SoC", description: `Each module owns exactly one concern, and the cut is made with the whole system in view, so the pieces still compose into one thing rather than a pile of tidy parts that no longer fit. Traded against ${faceLink(LEX_FACE, "over-layering", "over-layering")}: a separation that ${faceLink(TENSION_FACE, "over-layering-separation-of-concerns", "only adds indirection")} has cost more than it separated.`, term: "Separation of Concerns", }, { code: "KISS", description: "The simplest solution that meets the invariant, where simple means expressible as a rule or a generator rather than as an enumeration of cases. Compression is the test: a repeated shape is compressed by its type, and the simplest form is the one nothing can be removed from without breaking the invariant.", domains: [STRUCTURAL], term: "Simplicity", }, { code: "DRY", description: `A piece of logic or knowledge has one definition, and a repetition is compressed by its type: a literal into a constant, a structure into a composition, a behaviour into one orchestrator, a rule into one predicate. Mitigated against ${faceLink(LEX_FACE, "locality-of-behavior", "locality of behaviour")} by a ${faceLink(TENSION_FACE, "do-not-repeat-yourself-dry-locality-of-behavior", "rule that names the discriminator")}: semantics are centralised, incidental textual likeness stays local.`, term: "Do Not Repeat Yourself", }, { description: `A unit has one reason to change, and that reason is derived from an invariant it protects rather than from a feature it serves. Features cut across units; invariants belong to one. Traded against ${faceLink(LEX_FACE, "excessive-fragmentation", "excessive fragmentation")}: units ${faceLink(TENSION_FACE, "excessive-fragmentation-single-responsibility-principle-srp", "split finer than their invariants")} scatter one reason to change across many files.`, term: "Single Responsibility Principle", }, { description: "A unit is open for extension and closed for modification: new behaviour arrives through a few composable primitives, and the existing tested code is not edited to admit it.", term: "Open/Closed Principle", }, { description: "Anything that claims a type can stand in for it without a caller noticing. A substitute honours every behavioural guarantee of what it replaces.", term: "Liskov Substitution Principle", }, { description: "An interface is cut by usage: a client depends only on the operations it calls, never on a wide surface it mostly ignores.", term: "Interface Segregation Principle", }, { description: "High-level policy depends on abstractions, and the concrete details depend on the same abstractions, so the direction of dependency runs toward stability.", term: "Dependency Inversion Principle", }, { description: "A reference names what a thing means, never where it sits, so a move changes no reference and a location is never a dependency.", domains: [STRUCTURAL], term: "Semantic Addressing", }, { description: "Code, data and state share one representation, so a definition can be inspected and transformed like any other value, and the degree to which a system has that property is what the term names. Resolved by scope against Single Owner: ownership governs runtime resources, homoiconicity governs definitions.", term: "Homoiconicity", }, { description: `Every fact has one canonical, queryable home, and every other place it appears is a derivation of that home; a parallel truth is a bug waiting to disagree. Resolved by scope against ${faceLink(TENSION_FACE, "decentralization-single-source-of-truth", "Decentralisation")}: the truth is one, and the parties that read it are many.`, term: "Single Source of Truth", }, { description: "Order is a logical sequence or an append-only identifier, never a wall-clock timestamp, because a clock is a dependency on the host and a sequence depends on nothing.", domains: [STRUCTURAL], term: "Ordinal Time", }, { description: "Computed data is frozen after creation, and a variable is bound exactly once. Resolved by scope against State Over Code: computed outputs are immutable, resource state is mutable but managed.", term: "Immutability", }, { description: "Computation carries no persistent state: data flows through and nothing is retained between calls. Resolved by scope against State-Before-Mutation: computation is stateless, a resource is snapshotted before it changes.", term: "Statelessness", }, { description: "Repeating an operation produces the same result as running it once, so a retry is safe and a duplicate delivery has one effect.", term: "Idempotency", }, { description: "Specifying what is wanted rather than how to get it, leaving the how to the system that reads the specification and letting the system explain why while it runs.", domains: [COMPUTATION], term: "Declarative Specification", }, { description: "Units combine freely into larger units, and the combination has no special cases the parts did not have.", term: "Composability", }, { description: "Temporary inconsistency is allowed exactly while it is marked. Resolved by scope against Fail Fast: a broken resource invariant halts, a computation uncertainty is marked and carried on.", domains: [COMPUTATION], term: "Explicit Invalidity", }, { description: "Errors use the system's own vocabulary and are machine-processable, so a consumer dispatches on an error rather than parsing a sentence.", domains: [COMPUTATION], term: "Errors as Language", }, { description: "Every resource has exactly one owner responsible for its release, and every other reference to it is weak. Resolved by scope against Homoiconicity: ownership governs runtime resources, definitions are freely shared.", domains: [RESOURCE], term: "Single Owner", }, { description: "A resource lives no longer than its owner, and every open has its close. A lifetime nobody bounds is a leak waiting to be found.", domains: [RESOURCE], term: "Bounded Lifetime", }, { description: "Release is guaranteed by scope or structure, never by a person remembering, because release that depends on discipline eventually leaks.", domains: [RESOURCE], term: "Structural Release", }, { description: "Resource state is snapshotted before it changes, so a change can be reverted by reinstantiation rather than by patching. Resolved by scope against Statelessness: computation flows through, a resource is snapshotted.", domains: [RESOURCE], term: "State-Before-Mutation", }, { description: "A child never calls back into its parent. Communication runs the other way, by events the parent subscribes to, so the child stays free of a parent it should not know.", domains: [EXECUTION], term: "Don't Call Back", }, { description: `Components communicate by emitting events; the producer does not know its subscribers, and a pause or a resume is an ordinary control message. Traded against ${faceLink(LEX_FACE, "debuggability", "debuggability")}: a flow nobody can follow end to end is a flow nobody can fix, so ${faceLink(TENSION_FACE, "debuggability-event-driven-architecture", "tracing is bought explicitly")}.`, term: "Event-Driven Architecture", }, { description: "Append, never retract. A retraction is a second path every reader must handle; a correction is a new record that supersedes.", domains: [EXECUTION], term: "Monotonic Growth", }, { description: "A change in one area does not affect an unrelated one, so the effect of an edit is local by construction.", domains: [EXECUTION], term: "Orthogonality", }, { description: `An error is detected and reported at once, and invalid state halts rather than carrying on behind a fallback that masks it. Resolved by scope against Explicit Invalidity: a resource halts, a computation marks and continues. Traded against ${faceLink(TENSION_FACE, "fail-fast-graceful-degradation", "graceful degradation")}: a halt is chosen where it costs less than a wrong answer.`, term: "Fail Fast", }, { description: "Related logic lives together, bounded by what a person can hold in mind at once rather than by what a file system permits.", term: "High Cohesion", }, { description: "Dependencies between units are few and explicit, so a change stays where it was made.", term: "Loose Coupling", }, { description: `Internals are hidden behind an interface, and the interface is the only way in, but the system still ships with a live inspector, because hiding internals from callers is not the same as hiding them from whoever diagnoses the running thing. Traded against ${faceLink(LEX_FACE, "debuggability", "debuggability")}: the ${faceLink(TENSION_FACE, "debuggability-encapsulation", "inspector")} is where the operating point sits.`, term: "Encapsulation", }, { description: "Complexity stays under a declared bound the gate reads, and stopping with a stated uncertainty is preferred to continuing with a silent incorrectness.", domains: [HUMAN], term: "Bounded Complexity", }, { description: `Independent, swappable units with explicit boundaries, so a unit can be replaced without the rest knowing. Traded against ${faceLink(LEX_FACE, "cross-cutting-concerns", "cross-cutting concerns")}: a concern that ${faceLink(TENSION_FACE, "cross-cutting-concerns-modularity", "touches every unit")} is placed once, at the boundary, rather than spread through them.`, term: "Modularity", }, { description: "State is the asset and code is the replaceable part, so a design protects the state before it protects the code. Resolved by scope against Immutability: computed data is frozen, resource state is mutable but managed.", domains: [EVOLUTION], term: "State Over Code", }, { description: "An interface leaves room where the future is unknown, so a later feature is not blocked by a constraint nobody needed. Resolved by scope against YAGNI: the restraint applies to implementation, the openness to interfaces.", domains: [EVOLUTION], term: "Deliberate Under-Specification", }, { description: "You are not going to need it: nothing is built for a need nobody has. Resolved by scope against Deliberate Under-Specification: no code for hypothetical features, and no constraint that would block one.", domains: [EVOLUTION], term: "YAGNI", }, ]; const BOUNDARY_ENTRIES: readonly GlossaryEntry[] = [ { description: "Controls exist at every layer, so a single bypassed control exposes nothing on its own.", term: "Defense in Depth", }, { description: "Every actor holds the minimum authority its task needs, and anything not granted is denied.", term: "Least Privilege", }, { description: "The default configuration is the safe one; opening something up is a deliberate act.", term: "Secure by Default", }, { description: `A producer is bound to what its consumer can absorb, so load is refused or slowed at the source instead of piling up unseen. Traded against ${faceLink(TENSION_FACE, "backpressure-throughput", "throughput")}: the bound is a measured point, never a reflex.`, term: "Backpressure", }, { description: "Optimisation follows measurement: the bottleneck is profiled before anything is changed, and the change is judged by the measurement.", domains: [PERFORMANCE], term: "Profile First", }, { description: "Configuration comes from the environment, is validated at boot, and has no fallback default that would hide a missing value.", term: "Configuration Externalisation", }, { description: "An architectural invariant is a rule that runs, never a convention that is hoped for, so every stated rule has an executable check that fails the build when it is broken.", term: "Policy as Code", }, { description: "Preconditions, postconditions and invariants are stated and checked, never assumed.", term: "Design by Contract", }, { description: "The contract is defined before the implementation, so consumers depend on the interface and not on the first thing that happened to work.", term: "Contract-First Design", }, { description: "Types make invalid states unrepresentable, and every boundary validates its data against a schema.", term: "Type Safety", }, { description: `One canonical model from which every view derives. Traded against ${faceLink(LEX_FACE, "bounded-context-autonomy", "bounded context autonomy")}: one model holds ${faceLink(TENSION_FACE, "bounded-context-autonomy-canonical-model", "where contexts share meaning")}, and a translation stands where they do not.`, term: "Canonical Model", }, { description: "Code and domain share one vocabulary, so a name in the code is a name the domain expert would use.", term: "Ubiquitous Language", }, { description: "Behaviour matches what a reasonable reader expects from the name, and a surprise is a defect.", term: "Principle of Least Surprise", }, { description: "A change commits fully or not at all, within an explicit boundary.", term: "Atomicity" }, { description: `Every invariant that held before a change holds after it. Traded against ${faceLink(LEX_FACE, "availability", "availability")}: strong inside one transaction boundary, eventual across autonomy boundaries, and ${faceLink(TENSION_FACE, "availability-consistency", "where the point sits")} is a measured choice.`, term: "Consistency", }, { description: "Mutable state is confined to its owner, and side effects are explicit, ordered and bounded.", term: "State Isolation", }, { description: `Communication that does not wait for a reply, routed through a broker so the availability and the pace of both sides are decoupled. Traded against ${faceLink(LEX_FACE, "immediate-consistency", "immediate consistency")}: ${faceLink(TENSION_FACE, "asynchronous-communication-immediate-consistency", "synchronous inside a trust boundary")}, asynchronous across autonomy boundaries.`, term: "Asynchronous Communication", }, { description: "State is persisted as an immutable sequence of events, and the current state is a fold over them.", term: "Event Sourcing", }, { description: "Command query responsibility segregation: the write model and the read models are separate, so each can take the shape its job needs.", term: "CQRS", }, { description: "Bounded staleness accepted so that services stay independently available under partition; it is the availability side of the trade-off with Consistency, chosen where autonomy matters more than an immediate answer.", term: "Eventual Consistency", }, { description: "A service owns its data and its availability, and depends on no other service being up to do its own work.", term: "Service Autonomy", }, { description: "Order is defined by what depended on what, tracked with logical clocks, never by a wall clock.", term: "Causality", }, { description: "A running system emits machine-parseable signals, so its behaviour is legible from outside.", term: "Observability", }, { description: "The framework calls the code rather than the code calling the framework, so control flows from the outside in.", term: "Inversion of Control", }, { code: "DI", description: "Dependencies are handed to a unit rather than constructed inside it, so they are visible, replaceable and testable.", term: "Dependency Injection", }, { description: "New behaviour arrives as a plugin at a declared extension point, and the core stays unchanged.", term: "Plugin Architecture", }, { description: "Capabilities are registered in one queryable place and discovered from it, never located through a hidden dependency; the registry is the one list, and adding a capability is registering it rather than editing every caller.", term: "Registry Pattern", }, { description: "Configuration states what is wanted, and convention removes the boilerplate, so the only thing written down is the deviation from the default.", term: "Convention over Configuration", }, { description: "Definitions are held as inspectable, transformable data, so the same tooling that reads data can read code; it is the practice that Homoiconicity makes possible.", term: "Code as Data", }, { description: "Software structured around the domain rather than the framework, with the logic living in the model.", term: "Domain-Driven Design", }, { description: "A boundary inside which one model and one language hold, and outside which they may not.", term: "Bounded Context", }, { description: "Translation at the edge of a context, so a foreign model cannot leak in and corrupt the local one.", term: "Anti-Corruption Layer", }, { description: "The domain core depends on nothing; input, output and frameworks are adapters at the edge. Dependency inversion at the scale of a system.", term: "Ports and Adapters Architecture", }, { description: "A defined workflow driven by one coordinating owner, which knows every step and is the one place the sequence can be read.", term: "Orchestration", }, { description: "Autonomous reactions to events with no central conductor, used where the parts must stay independent and the sequence is allowed to be emergent.", term: "Choreography", }, { description: "A named design pattern is applied only when the force it answers is present in the code, never speculatively. A pattern with no force behind it is accidental complexity.", domains: [PATTERNS], term: "Pattern by Fit", }, { description: "A value is computed only when it is demanded, so nothing is materialised that nothing reads.", term: "Lazy Evaluation", }, { description: "The same inputs give the same outputs, and every source of nondeterminism is isolated and injected.", term: "Determinism", }, { description: "An expression can be replaced by its value without changing the program.", term: "Referential Transparency", }, { description: "A function whose only effect is its return value, so the core stays pure and the effects sit at the edge.", term: "Pure Functions", }, ]; export const GLOSSARY_ENTRIES: readonly GlossaryEntry[] = joinGlossary( [...CORE_ENTRIES, ...BOUNDARY_ENTRIES], VOCABULARY, ); const MAP_SECTION: Section = { icon: MAP_SECTION_ICON, id: MAP_SECTION_ID, intro: "Three maps place the terms below. The first is the layer diagram the whole page rests on, the second sets the stateless half of the core against the stateful half, and the third sets what holds inside one boundary against what holds across boundaries. A term sits where its rule holds whole, and two terms that seem to contradict are two terms on two sides of a line the map draws.", subsections: [ { blocks: [ { kind: "text", text: "Four core layers carry the rules about code itself. Computation is where data flows through and nothing is retained. Resource is where state lives and every handle has one owner. Execution is where the two meet through control flow and events. Structural applies to all of it and observes itself.", }, { kind: "text", text: "Resource observes computation, both feed execution, execution feeds structural, and structural feeds back into execution, which is the loop that lets a system read its own behaviour and correct it. Beneath the four sit human factors, which bound the whole by what a person can hold, and evolution, which says how the whole changes over time.", }, { caption: "The principle architecture: the four core layers, the observe and feedback arrows across them, and the two domains beneath.", kind: "mermaid", text: ARCHITECTURE_DIAGRAM, }, ], title: "The layers", }, { blocks: [ { kind: "text", text: "Five pairs of terms look like contradictions until the layer is read: statelessness against state before mutation, immutability against state over code, explicit invalidity against fail fast, deliberate under-specification against not building what nobody needs, homoiconicity against single ownership.", }, { kind: "text", text: "Each pair is two principles on two layers, so each is resolved by scope, and the edge between them names the boundary. A computation flows through while a resource is snapshotted. Computed data is frozen while resource state is managed. An uncertainty is marked in a computation while a broken invariant halts a resource.", }, { caption: "Stateless against stateful: each pair is resolved by scope, and the edge names the boundary that separates them.", kind: "mermaid", text: STATE_TENSION_DIAGRAM, }, ], title: "Stateless against stateful", }, { blocks: [ { kind: "text", text: "The distributed pairs live where one boundary ends. Consistency against availability, and asynchronous communication against immediate consistency, are trade-offs: strong or immediate inside one transaction boundary or trust boundary, eventual consistency across autonomy boundaries, with the operating point a measured choice. A canonical model against the autonomy of a bounded context is a trade-off too, one model where contexts share meaning and an anti-corruption layer where they do not.", }, { kind: "text", text: "Normalization against query performance separates by scope, the canonical store one way and the derived read models the other. Do not repeat yourself against locality of behaviour is the one pair a rule resolves, by asking whether a sameness is semantic or merely textual.", }, { caption: "Inside a boundary against across boundaries: the distributed pairs, each labelled with the mechanism that resolves it.", kind: "mermaid", text: BOUNDARY_TENSION_DIAGRAM, }, ], title: "Inside a boundary, across boundaries", }, ], title: "The principle architecture", }; const GLOSSARY_SECTION: Section = { blocks: [{ entries: GLOSSARY_ENTRIES, kind: "glossary" }], icon: GLOSSARY_SECTION_ICON, id: GLOSSARY_SECTION_ID, intro: "The principles this page teaches, one term per entry, with its short code where one is in common use and the layer this page places it on. An entry that names a record in the canon is joined to it: its kind is read from the record, and the term opens it, where the record's own placement can be read. The rest are the core layers' own terms. Where two entries pull against each other on one construct, the entry names its counterpart and the mechanism that resolves the pair, by scope, by a measured trade-off, or by a rule, whether the canon records that resolution or the two layers derive it.", subsections: [], title: "Architectural rules and principles", }; export const GLOSSARY_SECTIONS: readonly Section[] = [MAP_SECTION, GLOSSARY_SECTION];