Skip to content
Updated Sep 12, 2026 by Barča Dvořáková · Owner: analysisactiveconceptgeneric Edit on GitHub

Versioning ​

Concept layer — frozen. The Versioning generic. Nothing here is written by a normal playbook run; a project's own feature analysis is the live document and takes every edit. This layer names no project and links to none — the dependency runs one way, from an application to its concept.

Business Context ​

Business-Level Definition ​

Versioning gives an artefact whose content changes over time a governed home in which every past state remains retrievable. The artefact has a stable identity, a sequence of versions behind it, and exactly one version in effect for a given scope at a given moment. Changing which version is in effect is an ordinary, reversible act rather than an edit.

The artefact itself varies — instructions given to an automated process, a document or message template, a ruleset, a configuration profile. What does not vary is the shape of the problem: someone needs to change behaviour without a deployment, someone else needs to see what the behaviour was last week, and both need to be able to put it back.

Requirements Definition ​

  • A stable identity for the artefact, independent of any of its versions and of any scope.
  • Every change is preserved. Reverting means selecting an earlier version, not re-typing its content.
  • Exactly one version is in effect per identity per scope, and which one is unambiguous at all times.
  • Optionally, a default plus per-scope overrides, with a resolution order stated rather than implied.
  • Every lifecycle change is attributable: who did what, when.
  • Consumers resolve by identity, never by version id, and record the version they actually used.

Technical Context ​

User Stories / Use Cases ​

  1. As an editor, I save a change and get a new version, optionally putting it into effect immediately.
  2. As an editor, I revert by putting an earlier version back into effect.
  3. As the owner of a scope, I override the default for my scope, and remove the override to fall back to it.
  4. As a consumer, I resolve the version in effect for my identity and scope at the moment I run, and I record which version that was.
  5. As a reviewer, I read the full history of an artefact — which versions exist, which were in effect when, and who changed them.

Functional Requirements ​

Identity

  • The artefact is identified by a stable technical key, identical across all its versions and all its scopes. Consumers reference that key and nothing else; a consumer referencing a version id has pinned itself to a version and defeated the mechanism.
  • Where the identity is shown to people, its display name is content — either translated through the project's translation mechanism, or an untranslated operator-facing label where only administrators ever see it; a project says which. The key is never translated.
  • Anything that belongs to the identity rather than to a version — a display name, a description of purpose, the list of consumers — either lives in an identity table or is denormalised onto every version row with an invariant the project has to enforce: same value across every version and every scope, set when the identity is created, immutable afterwards. Denormalising is the cheap option, nothing in the database checks the invariant, and it is how an identity ends up with two names.
  • Whether identities are created through the management surface or introduced alongside the consumer that uses them is a project decision. Introducing them alongside the consumer keeps the set of identities equal to the set of things that actually read one, at the cost of a deployment to add one. Where the surface can create an identity, the identity's own uniqueness is a second read-then-write race on top of the label's — an existence pre-check is a fast path two concurrent creates both pass — and it needs the same serialisation as the label assignment; a partial unique index cannot express "first version of a key" on its own.

Immutability — a property to enforce, not a sentence to write

  • The concept requires that a version's content, once saved, never changes. A correction is a new version. Everything else here depends on that: rollback restores a known state only if the state is still what it was, an execution's record of "version 4" identifies something only if version 4 is fixed, and a history of edits nobody recorded is not a history.
  • Stating immutability does not create it. The common failure is a document that asserts versions are immutable beside an update path that edits them in place — often including the version currently in effect. When that happens the guarantee is false in exactly the cases that matter: behaviour changes with no version recording that it changed, a later rollback restores something other than what ran, and the audit trail shows an unbroken sequence that never happened.
  • A project therefore states where immutability is enforced, and picks one of:
    • No content update path at all. The only writes are an insert and a change to which version is in effect. Enforced below the application — a trigger rejecting content updates, a revoked update privilege, or an append-only store — so that a new call site cannot quietly reintroduce one.
    • A draft state. Content is editable only while a version has never been in effect; the transition to in-effect freezes it. Buys an editing experience without a version per keystroke, and costs a state machine that must be enforced in the same place the immutability is.
    • Metadata only. A description or changelog stays editable while the content columns are frozen. Simple, and it means the field that explains a version can be corrected without inventing one.
  • What is not an acceptable answer is enforcement in a single service method. That is one new caller away from being false, and nothing will report it when it becomes false.
  • A storage adapter that simply has no update method — the only implementation of a port that exposes insert, activate, withdraw and remove — is the application half of the first shape, and it is necessary and not sufficient. The storage half — a trigger, a revoked privilege, an append-only store — exists for the writes that do not go through the adapter: a migration, a support script, a second service. A project that has the first half and not the second has stated immutability, not enforced it.

Version sequence and label

  • Versions of one identity within one scope form a totally ordered sequence, and the order is derivable without consulting timestamps. Two versions created in the same second must still have an unambiguous predecessor and successor, or "the previous version" is not a well-defined phrase.
  • What the label is, is a project decision:
    • A monotonically increasing integer per identity and scope, assigned as the current maximum plus one. Ordering is free and people can say "roll back to 3". The cost is a read-then-write on every save, and a sequence that is scope-local — "version 3" names different content in two scopes.
    • An opaque, creation-ordered id. No contention and no arithmetic. The cost is that people cannot refer to a version conversationally, and every relative navigation becomes a query.
    • An author-supplied label — a semantic version, a release name. Expressive and meaningful to readers. The cost is that ordering is no longer derivable from the label, so the system must store an explicit order beside it, and nothing prevents two versions from claiming the same name.
  • Uniqueness within (identity, scope) is required whichever is chosen, and it is a database constraint rather than a check in application code. Assigning "maximum plus one" is a read-then-write race: two concurrent saves read the same maximum and both write it. Without the constraint the result is two versions bearing one label, every reference to that label is ambiguous from then on, and the corruption is silent — nothing fails at the moment it happens.

Exactly one version in effect

  • Per identity and per scope, at most one version is in effect. Activating a version withdraws the previously effective one, in the same transaction.
  • That uniqueness is enforced by a partial unique constraint in the database, not by the application's own logic. Two concurrent activations otherwise both succeed, resolution becomes nondeterministic, and which version ran depends on the query plan. With the constraint the race surfaces as a constraint violation, which is a bug report rather than a mystery.
  • Whether "nothing in effect" is a legal state is a decision. Permitting it means every consumer must handle absence and the system must define what happens when it occurs. Forbidding it means the last effective version can be replaced but never simply withdrawn, which is a simpler contract for consumers and a more constrained one for administrators.

Rollback

  • Rollback is putting an earlier version back into effect — the same operation as activation, not a separate mechanism.
  • Two shapes, and a project picks one. Re-pointing leaves the sequence untouched, so "we are back on version 3" is literally true and the sequence stays short; the sequence then no longer tells you what is current, because the highest number may not be in effect. Copying forward creates a new version with the old content, so the highest version is always the current one and the rollback is itself an event in the sequence; the cost is content duplicated in the history and a sequence that grows with every revert.

Removal

  • Removing a version is a soft delete at most, and the version in effect can never be removed.
  • Removal is in tension with traceability: if executions record the version they used, removing that version leaves those records pointing at nothing. A project decides whether history is removable at all, and if so whether a version any execution referenced is exempt.

Scope and resolution

  • Where the system has scopes, the usual arrangement is a default owned centrally plus overrides owned per scope. Resolution for (identity, scope) is: the scope's own effective version if one exists, otherwise the effective default.
  • The resolution order is stated explicitly and is deterministic. Consumers resolve at the moment of use, by identity and scope — not at start-up, or a configuration change requires a restart to take effect, which is most of what this generic exists to avoid.
  • An override is visible as an override, with its source. A scope that has silently diverged from an improved default is one of the two failure modes this generic reliably produces.

Guarding withdrawal — refuse early or fail late

  • Withdrawing an override or a default can leave a consumer with no version in effect at all. Both answers are defensible and a project chooses one:
    • Refuse at edit time. The system knows where each identity is referenced and rejects any operation that would strand a consumer, naming the consumers affected. The administrator learns immediately, at the moment they can still act. It requires a complete index of references — and an incomplete one weakens the guarantee silently, which is worse than not claiming it.
    • Fail at use time, with defined behaviour: refuse to run, or fall back to something stated. No reference index to maintain, and the failure lands on an end user in the middle of a process, far away in both time and person from whoever caused it.
  • The first is preferable wherever references are actually knowable. Where they are not — an identity referenced by data the system does not own, or by another system — claiming the first and delivering the second is the worst of the three options.
  • A consumer that resolves a fixed key from code is a reference the database does not hold. The project keeps a registry of those consumers beside the guard, and the failure mode is adding a consumer without registering it — nothing fails, the guard lets the withdrawal through. The cheapest detector is a test that every identity with a seeded version is either registered or referenced by data.
  • Choosing refuse-at-edit-time does not remove the use-time obligation. The guard prevents an administrator from creating the "nothing in effect" state; it does not make the state unreachable. Seeds, migrations, a scope provisioned without a default, an identity created but never activated all arrive at it by paths the guard does not see. The consumer's behaviour on resolving nothing — refuse to run, or fall back to something stated — is defined either way, and "no runtime error path is needed" is the assumption to distrust.

Traceability of use

  • Each consuming execution records the identity and the exact version it used. Without it a behaviour change cannot be attributed to a version, and the version history is decorative: it shows what could have run, never what did.

Composes other generics ​

This is a generic that mostly assembles others, and is a useful example of one:

  • an append-only audit trail records the lifecycle events — created, put into effect, withdrawn, removed — with actor and time. Versioning contributes its own event values to that vocabulary rather than building a history table of its own; a dedicated per-feature history is exactly the duplication the audit trail exists to prevent. It also inherits that trail's retention horizon, which is where the effect history below actually lives. Where executions record the version they used in the same trail, that horizon is also the horizon for "which version produced this outcome" — the retention decision for the audit log is a decision about traceability of use, and is made knowing that.
  • translations supply the display name of an identity.
  • the project's scoping generic supplies what "per scope" means, and its permission model supplies the split between who may edit a default and who may edit an override.

Internationalization & Localization ​

  • Where the identity's display name is translated, it resolves through the project's translation mechanism, for every supported locale; where the project has decided it is an untranslated operator-facing label (see Identity), it is stored once and no translation keys are seeded for it — seeding keys nothing reads is the usual sign that the decision was made twice. Version content and version descriptions are authored operational content and are not translated.
  • Where the artefact is itself locale-specific, the locale belongs in the scope, not in the content: a per-locale version of one identity resolves through the same mechanism as any other scoped override, and does not need a second one.

Non-Functional Requirements ​

  • Deterministic resolution: the same identity and scope resolve to the same version until someone changes which is in effect.
  • Scope isolation where scopes exist — a scope sees its own overrides and the defaults that apply to it, and nothing else.
  • Pagination on history reads.

Performance & Caching ​

Resolution sits on the hot path of every consuming execution, so how it is served is a real choice:

  • Resolve inline. One indexed lookup per execution, always current. Cheap enough for most workloads, and the default worth starting from.
  • Cache the effective version. Necessary at high call rates, and it introduces the failure this generic is least able to detect: a stale cache runs an old version silently, and the execution records that old version faithfully, so nothing looks wrong. A cache therefore needs invalidation on activation and a bounded lifetime, because a per-process cache receives no invalidation from an activation that happened elsewhere.

Transactional Operations ​

The following commit or roll back atomically:

  • Creating a version with immediate effect — the insert and the withdrawal of the previously effective version.
  • Putting a version into effect — the withdrawal and the activation.
  • The withdrawal guard, where a project refuses at edit time, and the write it guards: the check runs in the same transaction as the update, or a reference added concurrently slips between the two.

Concurrency safety rests on the partial unique constraint under Exactly one version in effect: a race between two activations surfaces as a constraint violation instead of two effective versions. The lifecycle record follows the write semantics the project chose for its audit trail.

API Analysis (API-A) ​

See child page: Versioning — API Analysis (API-A).

Domain Model ​

  • The versioned entity — one row per version, carrying: the identity key, the content, the version label and its ordering, the scope (with an explicit marker for the default), an in-effect flag, an optional description of what the version changes, plus the project's usual id and audit columns.
  • Constraints that are part of the model, not of the service layer: the label is unique within (identity, scope), and at most one row per (identity, scope) carries the in-effect flag.
  • Whether identities live in a table of their own or exist implicitly as the distinct keys present in the version table is a project decision. A separate table gives an identity somewhere to hold its description and its consumers, and gives "this key does not exist" a definite answer; deriving them keeps one table and lets a typo create an identity.

Logging ​

.info

  • version created (identity, scope, label, actor)
  • version put into effect or withdrawn (identity, scope, label, actor, previously effective version)
  • version removed (identity, scope, label, actor)

.warn

  • a withdrawal refused by the guard — the consumers that would have been stranded, and the actor.

The versioned content is usually operational configuration rather than business data. Two things attached to it carry weight where the artefact drives automated decisions — the record of which version produced a given outcome, and the trail of who changed what — and both are the first things asked for after a disputed outcome. The questions to settle:

  • Is the project obliged to show which version produced a given outcome? Where a regulated or contractual setting expects that traceability, the execution record is the only thing that supplies it, and it has to exist before the outcome is disputed rather than after.
  • How long must that record and the lifecycle trail be kept? Both live in the shared audit trail, whose retention horizon was set for other reasons. If an obligation here is longer than that horizon, the horizon is wrong for this concept.
  • Does the versioned content itself carry a preservation or disclosure constraint? Usually not, but an artefact encoding a published rule, a priced term or a regulated instruction can.

Cybersecurity Considerations ​

Access is gated by permissions distinct for the default scope and for a scope's own overrides — editing what every scope falls back to is a materially different act from editing one scope's override, and one permission for both understates it.

Data Privacy: the mechanism stores no personal identifiers of its own beyond the actor on each change, but whether the content holds personal data is a project's question rather than a property of the design — nothing prevents an author typing a name into it. Where the content instructs an automated process that will itself handle personal data, the responsibility that transfers to authors is not to embed personal data in the content: content is retained for as long as its history is, which is longer than most retention rules assume.


Risk Assessment ​

Business Risks ​

  • Regression. A new version can degrade the behaviour it governs. Mitigations are the whole point of the generic: preserved history, rollback as a single action, an audit trail naming who changed what, and each execution recording the version it used so a regression can be attributed rather than guessed at.
  • Scope drift. An override silently diverges from an improved default, leaving that scope on inferior content indefinitely. Mitigation: overrides are explicit and visible with their source, and removing one restores the default immediately. The residual risk is that nobody looks.

Technical Risks ​

  • Nothing in effect. An identity with no effective version blocks its consumer. Mitigations: every identity ships with a seeded initial version, and the withdrawal guard refuses to create the state.
  • Immutability asserted but unenforced. The failure described under Functional Requirements, and the one that damages every guarantee downstream of it at once. It has no mitigation after the fact — entries already written cannot be verified retroactively — so it is prevented, in the storage layer, or it is not prevented.

Auditing, Reporting & Measurement ​

Two histories, not one, and they answer different questions. The version history — what versions exist, who created them, what each changed — lives in the versioned entity itself and is free, because versions are never deleted and never edited. The effect history — which version was in effect, in which scope, between which moments — lives nowhere unless the lifecycle events are recorded, and it is the one people actually ask for after a behaviour changed.

One requirement follows, and it is the one most easily left out: an activation entry must name the version it withdrew as well as the one it installed. With only the new version recorded, the trail shows a sequence of activations from which the periods have to be reconstructed by replay, and a withdrawal that installed nothing leaves no mark at all, so "what was in effect at a given moment" becomes an inference rather than a read. Recording both ends makes each entry a closed interval. It also means the implicit withdrawal is not written as a separate withdrawal entry — one activation entry naming both versions, with the withdrawal value reserved for an explicit withdrawal that installed nothing; two entries per activation double-count the trail and make the two events look independent when they were one.

Traceability of use is the measurement axis unique to this concept. Because each consuming execution records the identity and the exact version it used, the trail can be asked what actually ran, not merely what was available to run. Three questions follow from that and nothing else: which versions ran and how much — a version in effect that no execution ever recorded is either unused or a resolution defect, and without the record the two are indistinguishable; how long a regression was live, counted in executions rather than minutes; and whether an outcome can be attributed to a version at all.

Reporting surfaces. Two, and both are browsing. Per identity — its versions, who changed them, which were in effect when — is the history a reviewer opens after a disputed outcome. Per scope, the effective version and its source — own override or fall-through to the default, and how far behind — is worth naming as a report because Risk Assessment offers exactly this as the mitigation for scope drift; if nobody builds the view, the mitigation is a sentence and the residual risk is the whole risk. Whether anything aggregates across identities is a project decision, and "browsing only" is a complete answer; leaving it unstated is not.

Three gaps.

  • Where the effective version is cached, the trail's timestamp is an upper bound, not the moment of change. The entry records when the activation committed; it cannot record when each replica stopped serving the previous version, because that happens on invalidation or expiry, elsewhere and later. During that window the executions' own recorded versions are the only honest evidence of the cutover — and they will disagree with the trail, correctly.
  • A removed version is still referenced by the executions that used it. Removal is a soft delete at most, so the row survives, but whether the reporting surfaces show it is a decision: hide it and every execution record pointing at it reads as a dangling reference; show it and the history includes versions an administrator believes they deleted.
  • Nothing here measures whether immutability held. The concept's most consequential technical risk is immutability asserted but unenforced, and it has no detection after the fact: an in-place edit leaves an unbroken sequence and a plausible trail. There is no report that finds it, which is precisely why enforcement has to sit in the storage layer rather than in a rule someone follows.

Known instances ​

None recorded in this copy. Add this project's instance here as it adopts the concept — one line naming the feature that implements it. This is the only place a concept may name a project, and the template ships it empty so a fork inherits no other project's entries.