Pattern 19: Version Control
Part II: Interaction Patterns - Temporal Patterns
Opening Scenario: The Contract That Needed a Do-Over
Brian was finalizing a major software licensing contract worth $500,000. He'd been negotiating with the client for three weeks. The current version looked perfect.
His manager, Karen, reviewed it and said: "This is good, but I think we gave up too much on the liability clause. What did Version 4 say about that?"
Brian checked the system:
Contract_Final.docx
Last modified: December 12, 2024
That was it. One file. No versions. No history.
"I don't know what Version 4 said," Brian admitted. "I've been overwriting the same file each time."
Karen's eyes widened. "You don't have the previous versions?"
"No. I just saved changes as I went."
Three weeks of negotiation. Twelve rounds of revisions. All lost except the final version.
Karen remembered: "Version 4 had better liability terms. We need to get those back and combine them with the pricing from Version 7."
They couldn't. The intermediate versions were gone forever.
Brian had to restart negotiations from scratch. Two more weeks. The client threatened to walk away. The deal nearly died.
The legal department's IT manager, Susan, heard about this disaster. She investigated how contracts were being managed:
// Bad: Overwrites previous versions
function saveContract(contractId, content) {
database.update(contractId, {
content: content,
lastModified: new Date()
});
// Previous content is lost forever
// Can't undo changes
// Can't compare versions
// Can't restore previous state
}
She found more horror stories:
Accidental deletion: - Lawyer deleted critical paragraph - Saved file - Realized mistake next day - Paragraph gone forever
Concurrent editing: - Two lawyers editing same contract - Both saved changes - Second save overwrote first lawyer's work - Hours of work lost
"What did we agree to?": - Client: "You changed this clause after we approved it" - Company: "No we didn't" - No way to prove who was right - Trust broken, relationship damaged
Susan built a comprehensive version control system:
class VersionControlSystem {
saveVersion(document, content, metadata) {
const version = {
versionNumber: this.getNextVersionNumber(document.id),
documentId: document.id,
content: content,
timestamp: new Date().toISOString(),
author: metadata.userId,
authorName: metadata.userName,
comment: metadata.comment,
significant: metadata.significant || false,
tags: metadata.tags || [],
parentVersion: this.getCurrentVersion(document.id),
hash: this.computeHash(content)
};
// Store version permanently
this.versionStore.insert(version);
// Update current pointer
this.updateCurrentVersion(document.id, version.versionNumber);
return version;
}
getVersion(documentId, versionNumber) {
return this.versionStore.query({
documentId: documentId,
versionNumber: versionNumber
});
}
getAllVersions(documentId) {
return this.versionStore.query({
documentId: documentId,
orderBy: 'versionNumber DESC'
});
}
compareVersions(documentId, version1, version2) {
const v1 = this.getVersion(documentId, version1);
const v2 = this.getVersion(documentId, version2);
return this.diff(v1.content, v2.content);
}
restoreVersion(documentId, versionNumber, metadata) {
const version = this.getVersion(documentId, versionNumber);
// Create new version with old content
return this.saveVersion(
{ id: documentId },
version.content,
{
...metadata,
comment: `Restored from version ${versionNumber}: ${version.comment}`
}
);
}
}
Now when Brian worked on contracts:
Contract #5847 - Software Licensing Agreement
Current Version: 12
Last Modified: Dec 12, 2024 3:47 PM by Brian Wilson
[Version History]
┌────────────────────────────────────────────────────┐
│ v12 Dec 12, 3:47 PM - Brian Wilson │
│ "Final version with approved pricing" │
│ │
│ v11 Dec 11, 4:23 PM - Brian Wilson │
│ "Revised liability clause per legal review" │
│ │
│ v10 Dec 11, 2:15 PM - Karen Chen (Manager) │
│ "Adjusted payment terms" │
│ │
│ v7 Dec 9, 11:30 AM - Brian Wilson ⭐ │
│ "Best pricing agreed - TAG: pricing-approved" │
│ │
│ v4 Dec 6, 2:00 PM - Brian Wilson ⭐ │
│ "Liability terms acceptable - TAG: legal-ok" │
└────────────────────────────────────────────────────┘
[Compare] [Restore] [Tag Version]
When Karen asked about Version 4:
[Comparing Version 4 vs Version 12]
Section 7: Liability Limitations
Version 4 (Dec 6):
"Company's total liability shall not exceed the greater of
(i) amount paid under this Agreement or (ii) $50,000."
Version 12 (Dec 12):
"Company's total liability shall not exceed the amount
paid under this Agreement in the 12 months preceding
the claim."
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ANALYSIS: Version 4 had $50,000 floor. Version 12 removed it.
[Restore Version 4 Language] [Create Hybrid Version]
Brian could see exactly what changed. He created Version 13 combining: - Liability language from Version 4 (better protection) - Pricing from Version 7 (agreed terms) - Final edits from Version 12 (polish)
Do-overs made possible. Contract saved. Deal closed. Everyone happy.
Context
Version Control applies when:
Documents evolve over time: Multiple rounds of editing and revision
Mistakes happen: Need ability to undo changes
Collaboration required: Multiple people editing same document
Comparison needed: "What changed between these two versions?"
Compliance mandates: Must track all changes for audit
Experimentation wanted: Try changes without fear of breaking things
Historical reference: Need to see "what did we agree to in March?"
Rollback capability: Return to previous working state
Problem Statement
Most systems lack proper version control, causing data loss and workflow problems:
Overwriting data:
// Bad: Destroys previous versions
UPDATE documents
SET content = 'New content'
WHERE id = 123;
// Old content is gone
// Can't undo
// Can't compare
// Can't restore
No version history:
// Bad: Only current state
{
id: 123,
content: "Current content",
lastModified: "2024-12-12"
}
// How many times edited?
// Who made previous changes?
// What did it look like yesterday?
// Unknown!
Lost intermediate work:
Version 1: Original draft
Version 2: Manager edits (lost)
Version 3: Legal review (lost)
Version 4: Client feedback (lost)
Version 5: Final (only this exists)
// All the intermediate wisdom is gone
// Can't see evolution of thinking
// Can't learn from process
No comparison capability:
// Bad: Can't see what changed
function whatChanged(oldDoc, newDoc) {
return "Something changed, but we don't know what";
}
// Users confused about differences
// Can't review changes effectively
// Disputes over "who changed what"
Concurrent edit conflicts:
User A: Opens document at 2:00 PM
User B: Opens document at 2:05 PM
User A: Saves changes at 2:30 PM
User B: Saves changes at 2:35 PM
Result: User A's work is lost
Only User B's changes survive
No rollback:
// Bad: Can't undo bulk changes
function deleteAllComments() {
document.comments = [];
save(document);
}
// Comments deleted
// No way to get them back
// Irreversible mistake
We need comprehensive version control that preserves history, enables comparison, supports collaboration, and allows safe experimentation.
Forces
Storage vs History
- Complete version history uses storage
- But history is valuable
- Balance retention with costs
Granularity vs Usability
- Version every keystroke?
- Or only manual saves?
- Balance detail with practicality
Automatic vs Manual
- Auto-version provides safety net
- But creates version spam
- Balance protection with clarity
Linear vs Branching
- Linear history is simple
- But collaboration needs branches
- Balance simplicity with power
Performance vs Completeness
- Storing full copies is slow
- But diffs can be complex
- Balance speed with reliability
Solution
Implement comprehensive version control system that automatically preserves every save as immutable version, enables comparison between any versions, supports restoration of previous states, and provides clear visualization of document evolution.
The pattern has seven key strategies:
1. Immutable Version Storage
Store every version permanently:
class ImmutableVersionStore {
constructor() {
this.versions = new Map();
this.currentPointers = new Map();
}
saveVersion(documentId, content, metadata) {
const versionNumber = this.getNextVersionNumber(documentId);
const version = {
id: this.generateVersionId(),
versionNumber: versionNumber,
documentId: documentId,
// Content (immutable)
content: Object.freeze(this.deepClone(content)),
contentHash: this.hashContent(content),
contentSize: JSON.stringify(content).length,
// Metadata
timestamp: new Date().toISOString(),
author: metadata.author,
authorName: metadata.authorName,
authorEmail: metadata.authorEmail,
// Version info
comment: metadata.comment || '',
significant: metadata.significant || false,
tags: metadata.tags || [],
// Lineage
parentVersion: this.getCurrentVersion(documentId),
childVersions: [],
// Storage optimization
storageType: this.determineStorageType(documentId, content),
delta: null, // For delta storage
baseVersion: null // For delta storage
};
// Update parent's children list
if (version.parentVersion) {
const parent = this.getVersion(documentId, version.parentVersion);
parent.childVersions.push(versionNumber);
}
// Store based on strategy
if (version.storageType === 'full') {
this.storeFullVersion(version);
} else {
this.storeDeltaVersion(version, content);
}
// Update current pointer
this.currentPointers.set(documentId, versionNumber);
return version;
}
determineStorageType(documentId, content) {
const versions = this.getVersions(documentId);
// Store full version every 10 versions
if (versions.length % 10 === 0) {
return 'full';
}
// Store full if content dramatically different
const current = this.getCurrentVersionContent(documentId);
if (current) {
const similarity = this.calculateSimilarity(current, content);
if (similarity < 0.3) {
return 'full'; // Less than 30% similar
}
}
return 'delta'; // Store as delta
}
storeFullVersion(version) {
this.versions.set(version.id, version);
}
storeDeltaVersion(version, content) {
const parent = this.getVersion(
version.documentId,
version.parentVersion
);
// Compute delta from parent
const delta = this.computeDelta(parent.content, content);
version.delta = delta;
version.baseVersion = version.parentVersion;
version.content = null; // Don't store full content
this.versions.set(version.id, version);
}
getVersion(documentId, versionNumber) {
const versions = Array.from(this.versions.values()).filter(v =>
v.documentId === documentId && v.versionNumber === versionNumber
);
if (versions.length === 0) {
return null;
}
const version = versions[0];
// If delta-stored, reconstruct content
if (version.storageType === 'delta') {
version.content = this.reconstructContent(version);
}
return version;
}
reconstructContent(version) {
// Get base version
const base = this.getVersion(version.documentId, version.baseVersion);
// Apply delta
return this.applyDelta(base.content, version.delta);
}
computeDelta(oldContent, newContent) {
// Simplified - real implementation would use sophisticated diff
return {
type: 'json-patch',
patches: this.jsonDiff(oldContent, newContent)
};
}
applyDelta(baseContent, delta) {
// Apply patches to reconstruct content
let content = this.deepClone(baseContent);
delta.patches.forEach(patch => {
this.applyPatch(content, patch);
});
return content;
}
}
2. Version Comparison
Compare any two versions:
class VersionComparison {
compareVersions(version1, version2) {
const comparison = {
version1: {
number: version1.versionNumber,
timestamp: version1.timestamp,
author: version1.authorName
},
version2: {
number: version2.versionNumber,
timestamp: version2.timestamp,
author: version2.authorName
},
differences: this.findDifferences(version1.content, version2.content),
statistics: this.calculateStatistics(version1.content, version2.content)
};
return comparison;
}
findDifferences(content1, content2) {
const diffs = [];
// Text comparison
if (typeof content1 === 'string' && typeof content2 === 'string') {
return this.textDiff(content1, content2);
}
// Object comparison
if (typeof content1 === 'object' && typeof content2 === 'object') {
return this.objectDiff(content1, content2);
}
return [{
type: 'type-change',
from: typeof content1,
to: typeof content2
}];
}
textDiff(text1, text2) {
// Use diff algorithm (Myers, Hunt-McIlroy, etc.)
const lines1 = text1.split('\n');
const lines2 = text2.split('\n');
const changes = [];
let i = 0, j = 0;
while (i < lines1.length || j < lines2.length) {
if (i >= lines1.length) {
// Addition
changes.push({
type: 'addition',
lineNumber: j + 1,
content: lines2[j]
});
j++;
} else if (j >= lines2.length) {
// Deletion
changes.push({
type: 'deletion',
lineNumber: i + 1,
content: lines1[i]
});
i++;
} else if (lines1[i] === lines2[j]) {
// No change
i++;
j++;
} else {
// Modification
changes.push({
type: 'modification',
lineNumber: i + 1,
oldContent: lines1[i],
newContent: lines2[j]
});
i++;
j++;
}
}
return changes;
}
objectDiff(obj1, obj2) {
const changes = [];
// Check all keys in obj1
Object.keys(obj1).forEach(key => {
if (!(key in obj2)) {
changes.push({
type: 'field-removed',
field: key,
oldValue: obj1[key]
});
} else if (obj1[key] !== obj2[key]) {
changes.push({
type: 'field-modified',
field: key,
oldValue: obj1[key],
newValue: obj2[key]
});
}
});
// Check for new keys in obj2
Object.keys(obj2).forEach(key => {
if (!(key in obj1)) {
changes.push({
type: 'field-added',
field: key,
newValue: obj2[key]
});
}
});
return changes;
}
calculateStatistics(content1, content2) {
const stats = {
additions: 0,
deletions: 0,
modifications: 0
};
const diffs = this.findDifferences(content1, content2);
diffs.forEach(diff => {
if (diff.type === 'addition' || diff.type === 'field-added') {
stats.additions++;
} else if (diff.type === 'deletion' || diff.type === 'field-removed') {
stats.deletions++;
} else if (diff.type === 'modification' || diff.type === 'field-modified') {
stats.modifications++;
}
});
stats.totalChanges = stats.additions + stats.deletions + stats.modifications;
return stats;
}
renderDiff(comparison) {
let html = '<div class="version-comparison">';
html += `
<div class="comparison-header">
<div class="version-info">
<strong>Version ${comparison.version1.number}</strong>
${comparison.version1.timestamp}
by ${comparison.version1.author}
</div>
<div class="comparison-arrow">→</div>
<div class="version-info">
<strong>Version ${comparison.version2.number}</strong>
${comparison.version2.timestamp}
by ${comparison.version2.author}
</div>
</div>
`;
html += '<div class="statistics">';
html += `<span class="additions">+${comparison.statistics.additions}</span>`;
html += `<span class="deletions">-${comparison.statistics.deletions}</span>`;
html += `<span class="modifications">~${comparison.statistics.modifications}</span>`;
html += '</div>';
html += '<div class="differences">';
comparison.differences.forEach(diff => {
html += this.renderDifference(diff);
});
html += '</div>';
html += '</div>';
return html;
}
renderDifference(diff) {
if (diff.type === 'addition') {
return `
<div class="diff-line addition">
<span class="line-number">+${diff.lineNumber}</span>
<span class="content">${diff.content}</span>
</div>
`;
}
if (diff.type === 'deletion') {
return `
<div class="diff-line deletion">
<span class="line-number">-${diff.lineNumber}</span>
<span class="content">${diff.content}</span>
</div>
`;
}
if (diff.type === 'modification') {
return `
<div class="diff-line modification">
<div class="old">
<span class="line-number">${diff.lineNumber}</span>
<del>${diff.oldContent}</del>
</div>
<div class="new">
<span class="line-number">${diff.lineNumber}</span>
<ins>${diff.newContent}</ins>
</div>
</div>
`;
}
}
}
3. Version Restoration
Restore any previous version:
class VersionRestoration {
constructor(versionStore) {
this.versionStore = versionStore;
}
restoreVersion(documentId, targetVersion, metadata) {
// Get the version to restore
const version = this.versionStore.getVersion(documentId, targetVersion);
if (!version) {
throw new Error(`Version ${targetVersion} not found`);
}
// Create new version with restored content
const restoredVersion = this.versionStore.saveVersion(
documentId,
version.content,
{
author: metadata.userId,
authorName: metadata.userName,
authorEmail: metadata.userEmail,
comment: `Restored from version ${targetVersion}: ${version.comment}`,
significant: true,
tags: ['restored', `from-v${targetVersion}`]
}
);
return {
restoredVersion: restoredVersion.versionNumber,
restoredFrom: targetVersion,
restoredContent: version.content,
restoredBy: metadata.userName,
restoredAt: new Date()
};
}
createHybridVersion(documentId, sourceVersions, metadata) {
// Combine content from multiple versions
const versions = sourceVersions.map(vNum =>
this.versionStore.getVersion(documentId, vNum)
);
// Merge content (domain-specific logic)
const hybridContent = this.mergeVersions(versions, metadata.mergeStrategy);
// Save as new version
const hybridVersion = this.versionStore.saveVersion(
documentId,
hybridContent,
{
...metadata,
comment: `Hybrid version from v${sourceVersions.join(', v')}`,
significant: true,
tags: ['hybrid', ...sourceVersions.map(v => `from-v${v}`)]
}
);
return hybridVersion;
}
mergeVersions(versions, strategy) {
if (strategy === 'field-by-field') {
return this.mergeFieldByField(versions);
}
if (strategy === 'section-by-section') {
return this.mergeSectionBySection(versions);
}
// Default: use most recent non-null values
return this.mergeLatestValues(versions);
}
mergeFieldByField(versions) {
const merged = {};
// For each field, take value from specified version
// (metadata would specify which version for each field)
return merged;
}
revertChanges(documentId, fromVersion, toVersion, metadata) {
// Revert specific changes made between versions
const from = this.versionStore.getVersion(documentId, fromVersion);
const to = this.versionStore.getVersion(documentId, toVersion);
// Find what changed
const changes = this.findChanges(from.content, to.content);
// Revert those changes from current version
const current = this.versionStore.getCurrentVersionContent(documentId);
const reverted = this.applyRevert(current, changes);
return this.versionStore.saveVersion(
documentId,
reverted,
{
...metadata,
comment: `Reverted changes from v${fromVersion} to v${toVersion}`,
tags: ['revert']
}
);
}
}
4. Version Tagging
Mark significant versions:
class VersionTagging {
constructor(versionStore) {
this.versionStore = versionStore;
this.tags = new Map();
}
tagVersion(documentId, versionNumber, tag, metadata) {
const version = this.versionStore.getVersion(documentId, versionNumber);
if (!version.tags) {
version.tags = [];
}
const tagInfo = {
tag: tag,
taggedBy: metadata.userId,
taggedByName: metadata.userName,
taggedAt: new Date().toISOString(),
description: metadata.description || ''
};
version.tags.push(tagInfo);
// Store in tag index for fast lookup
const tagKey = `${documentId}:${tag}`;
this.tags.set(tagKey, versionNumber);
return tagInfo;
}
getTaggedVersion(documentId, tag) {
const tagKey = `${documentId}:${tag}`;
const versionNumber = this.tags.get(tagKey);
if (!versionNumber) {
return null;
}
return this.versionStore.getVersion(documentId, versionNumber);
}
// Common tags
tagAsApproved(documentId, versionNumber, approver) {
return this.tagVersion(documentId, versionNumber, 'approved', {
userId: approver.id,
userName: approver.name,
description: 'Officially approved version'
});
}
tagAsRelease(documentId, versionNumber, releaseInfo) {
return this.tagVersion(documentId, versionNumber, `release-${releaseInfo.version}`, {
userId: releaseInfo.releasedBy.id,
userName: releaseInfo.releasedBy.name,
description: `Release ${releaseInfo.version}: ${releaseInfo.notes}`
});
}
tagAsMilestone(documentId, versionNumber, milestone) {
return this.tagVersion(documentId, versionNumber, milestone, {
userId: milestone.userId,
userName: milestone.userName,
description: milestone.description
});
}
getAllTags(documentId) {
const versions = this.versionStore.getAllVersions(documentId);
const allTags = [];
versions.forEach(version => {
if (version.tags && version.tags.length > 0) {
version.tags.forEach(tag => {
allTags.push({
versionNumber: version.versionNumber,
tag: tag.tag,
taggedBy: tag.taggedByName,
taggedAt: tag.taggedAt,
description: tag.description
});
});
}
});
return allTags;
}
}
5. Branching and Merging
Support parallel development:
class VersionBranching {
constructor(versionStore) {
this.versionStore = versionStore;
this.branches = new Map();
}
createBranch(documentId, fromVersion, branchName, metadata) {
const branch = {
id: this.generateBranchId(),
name: branchName,
documentId: documentId,
baseVersion: fromVersion,
createdBy: metadata.userId,
createdAt: new Date().toISOString(),
description: metadata.description || '',
headVersion: fromVersion // Latest version in this branch
};
this.branches.set(branch.id, branch);
return branch;
}
saveVersionToBranch(branchId, content, metadata) {
const branch = this.branches.get(branchId);
// Save version with branch reference
const version = this.versionStore.saveVersion(
branch.documentId,
content,
{
...metadata,
branch: branchId,
branchName: branch.name
}
);
// Update branch head
branch.headVersion = version.versionNumber;
return version;
}
mergeBranch(sourceBranchId, targetBranchId, metadata) {
const sourceBranch = this.branches.get(sourceBranchId);
const targetBranch = this.branches.get(targetBranchId);
// Get head versions of both branches
const sourceHead = this.versionStore.getVersion(
sourceBranch.documentId,
sourceBranch.headVersion
);
const targetHead = this.versionStore.getVersion(
targetBranch.documentId,
targetBranch.headVersion
);
// Find common ancestor
const ancestor = this.findCommonAncestor(sourceHead, targetHead);
// Perform three-way merge
const merged = this.threeWayMerge(
ancestor.content,
sourceHead.content,
targetHead.content
);
if (merged.conflicts.length > 0) {
return {
status: 'conflicts',
conflicts: merged.conflicts,
mergedContent: merged.content
};
}
// Save merged version to target branch
const mergedVersion = this.saveVersionToBranch(
targetBranchId,
merged.content,
{
...metadata,
comment: `Merged ${sourceBranch.name} into ${targetBranch.name}`,
tags: ['merge'],
mergedFrom: sourceBranchId
}
);
return {
status: 'success',
mergedVersion: mergedVersion.versionNumber,
conflicts: []
};
}
threeWayMerge(base, source, target) {
const conflicts = [];
const merged = this.deepClone(base);
// Find changes in source
const sourceChanges = this.findChanges(base, source);
// Find changes in target
const targetChanges = this.findChanges(base, target);
// Apply non-conflicting changes
sourceChanges.forEach(change => {
const conflict = this.findConflict(change, targetChanges);
if (conflict) {
conflicts.push({
field: change.field,
baseValue: base[change.field],
sourceValue: source[change.field],
targetValue: target[change.field]
});
} else {
merged[change.field] = source[change.field];
}
});
targetChanges.forEach(change => {
if (!this.wasApplied(change, sourceChanges)) {
merged[change.field] = target[change.field];
}
});
return {
content: merged,
conflicts: conflicts
};
}
findCommonAncestor(version1, version2) {
// Walk back through version history to find common ancestor
const ancestors1 = this.getAncestors(version1);
const ancestors2 = this.getAncestors(version2);
// Find first common version
for (const v1 of ancestors1) {
if (ancestors2.some(v2 => v2.versionNumber === v1.versionNumber)) {
return v1;
}
}
return null;
}
getAncestors(version) {
const ancestors = [];
let current = version;
while (current.parentVersion) {
current = this.versionStore.getVersion(
current.documentId,
current.parentVersion
);
ancestors.push(current);
}
return ancestors;
}
}
6. Automatic Versioning
Auto-save versions based on rules:
class AutomaticVersioning {
constructor(versionStore) {
this.versionStore = versionStore;
this.config = {
autoSaveInterval: 5 * 60 * 1000, // 5 minutes
significantChangeThreshold: 100, // 100 chars
maxAutoVersions: 10
};
}
setupAutoSave(documentId, editor, metadata) {
let lastSaved = Date.now();
let lastContent = editor.getContent();
setInterval(() => {
const currentContent = editor.getContent();
// Check if significant change
if (this.isSignificantChange(lastContent, currentContent)) {
// Auto-save version
this.versionStore.saveVersion(
documentId,
currentContent,
{
...metadata,
comment: 'Auto-saved',
significant: false,
tags: ['auto-save']
}
);
lastSaved = Date.now();
lastContent = currentContent;
// Cleanup old auto-save versions
this.cleanupAutoVersions(documentId);
}
}, this.config.autoSaveInterval);
}
isSignificantChange(oldContent, newContent) {
const oldStr = JSON.stringify(oldContent);
const newStr = JSON.stringify(newContent);
const diff = Math.abs(oldStr.length - newStr.length);
return diff >= this.config.significantChangeThreshold;
}
cleanupAutoVersions(documentId) {
const versions = this.versionStore.getAllVersions(documentId);
const autoVersions = versions
.filter(v => v.tags && v.tags.includes('auto-save'))
.sort((a, b) => b.versionNumber - a.versionNumber);
// Keep only last N auto-save versions
const toDelete = autoVersions.slice(this.config.maxAutoVersions);
toDelete.forEach(version => {
// Mark as archived or delete if safe
this.archiveVersion(version);
});
}
versionBeforeCriticalOperation(documentId, operation, metadata) {
// Create checkpoint before risky operations
const current = this.versionStore.getCurrentVersionContent(documentId);
return this.versionStore.saveVersion(
documentId,
current,
{
...metadata,
comment: `Before ${operation}`,
significant: true,
tags: ['checkpoint', `before-${operation}`]
}
);
}
}
7. Version History Visualization
Show version timeline:
class VersionVisualization {
renderVersionHistory(documentId) {
const versions = this.versionStore.getAllVersions(documentId);
return `
<div class="version-history">
<div class="history-header">
<h3>Version History</h3>
<div class="history-stats">
<span>${versions.length} versions</span>
<span>${this.getUniqueAuthors(versions)} contributors</span>
</div>
</div>
<div class="version-timeline">
${versions.map(v => this.renderVersionNode(v)).join('')}
</div>
</div>
`;
}
renderVersionNode(version) {
return `
<div class="version-node ${version.significant ? 'significant' : 'auto'}">
<div class="version-header">
<span class="version-number">v${version.versionNumber}</span>
${version.tags.map(t => `<span class="tag">${t}</span>`).join('')}
</div>
<div class="version-meta">
<span class="author">${version.authorName}</span>
<span class="timestamp">${this.formatTime(version.timestamp)}</span>
</div>
${version.comment ? `
<div class="version-comment">${version.comment}</div>
` : ''}
<div class="version-actions">
<button onclick="viewVersion(${version.versionNumber})">View</button>
<button onclick="compareVersion(${version.versionNumber})">Compare</button>
<button onclick="restoreVersion(${version.versionNumber})">Restore</button>
</div>
</div>
`;
}
renderBranchDiagram(documentId) {
const branches = this.getAllBranches(documentId);
// Draw branch diagram (like git log --graph)
return this.drawBranchGraph(branches);
}
}
Implementation Details
Complete Version Control System
class ComprehensiveVersionControl {
constructor() {
this.versionStore = new ImmutableVersionStore();
this.comparison = new VersionComparison();
this.restoration = new VersionRestoration(this.versionStore);
this.tagging = new VersionTagging(this.versionStore);
this.branching = new VersionBranching(this.versionStore);
this.autoVersion = new AutomaticVersioning(this.versionStore);
this.visualization = new VersionVisualization();
}
// Save new version
save(documentId, content, metadata) {
return this.versionStore.saveVersion(documentId, content, metadata);
}
// Get specific version
get(documentId, versionNumber) {
return this.versionStore.getVersion(documentId, versionNumber);
}
// Compare two versions
compare(documentId, version1, version2) {
const v1 = this.get(documentId, version1);
const v2 = this.get(documentId, version2);
return this.comparison.compareVersions(v1, v2);
}
// Restore previous version
restore(documentId, versionNumber, metadata) {
return this.restoration.restoreVersion(documentId, versionNumber, metadata);
}
// Tag important version
tag(documentId, versionNumber, tag, metadata) {
return this.tagging.tagVersion(documentId, versionNumber, tag, metadata);
}
// List all versions
history(documentId) {
return this.versionStore.getAllVersions(documentId);
}
}
// Usage
const versionControl = new ComprehensiveVersionControl();
// Save version
versionControl.save(
'contract-5847',
contractContent,
{
author: user.id,
authorName: user.name,
authorEmail: user.email,
comment: 'Revised liability clause per legal review',
significant: true
}
);
// Compare versions
const comparison = versionControl.compare('contract-5847', 4, 12);
console.log(`${comparison.statistics.totalChanges} changes found`);
// Restore version
versionControl.restore(
'contract-5847',
7, // Version 7 had best pricing
{
userId: user.id,
userName: user.name,
userEmail: user.email
}
);
// Tag approved version
versionControl.tag(
'contract-5847',
15,
'approved',
{
userId: manager.id,
userName: manager.name,
description: 'Final approved version'
}
);
Consequences
Benefits
Safety Net: - Every change preserved - Can always undo - Experimentation safe
Collaboration: - Multiple people can edit - Changes tracked per person - Merge conflicts resolved
Accountability: - Who made which changes - When were they made - Why were they made
Comparison: - See exactly what changed - Understand evolution - Learn from process
Recovery: - Restore any previous state - Recover from mistakes - Find working version
Liabilities
Storage Costs: - Every version uses space - Can grow large - Need archival strategy
Complexity: - More complex than simple save - Branching/merging advanced - Learning curve exists
Performance: - Storing versions takes time - Comparison can be slow - Need optimization
Version Proliferation: - Too many versions confuse - Auto-save creates clutter - Need cleanup strategies
Merge Conflicts: - Concurrent edits conflict - Manual resolution needed - Can be frustrating
Domain Examples
Legal: Contract Versioning
// Track contract negotiations
versionControl.save(contractId, content, {
comment: 'Initial draft based on standard template',
tags: ['draft', 'template-based']
});
versionControl.save(contractId, content, {
comment: 'Client requested changes to payment terms',
tags: ['client-revision']
});
versionControl.save(contractId, content, {
comment: 'Legal review - liability clause updated',
tags: ['legal-review'],
significant: true
});
versionControl.tag(contractId, 12, 'final-approved', {
description: 'Version sent to client for signature'
});
Software: Code Versioning
// Development workflow
versionControl.createBranch(codeId, 10, 'feature-authentication', {
description: 'Adding OAuth authentication'
});
versionControl.saveVersionToBranch(branchId, code, {
comment: 'Implemented OAuth login flow'
});
versionControl.mergeBranch(featureBranch, mainBranch, {
comment: 'Merging authentication feature'
});
Content: Article Editing
// Editorial process
versionControl.save(articleId, content, {
comment: 'First draft',
tags: ['draft']
});
versionControl.save(articleId, content, {
comment: 'Editor revisions - structure improved',
tags: ['editorial-review']
});
versionControl.save(articleId, content, {
comment: 'Final copy edit - typos fixed',
tags: ['copy-edit']
});
versionControl.tag(articleId, 8, 'published', {
description: 'Published on website'
});
Related Patterns
Prerequisites: - Volume 3, Pattern 17: State-Aware Behavior (versions have states) - Volume 3, Pattern 18: Audit Trail (versions are audit points)
Synergies: - Volume 3, Pattern 11: Cascading Updates (version cascades) - All patterns (versioning applies to all form data)
Conflicts: - Real-time collaboration (too many versions) - Performance-critical apps (overhead high)
Alternatives: - Event sourcing (store events not versions) - Database timestamps (simple audit) - Manual backups (periodic snapshots)
Known Uses
Git: Source code version control
Google Docs: Document revision history
Microsoft Word: Track Changes and version history
Confluence: Page version history
SharePoint: Document versions
Salesforce: Field history tracking
GitHub: Pull requests and commit history
Dropbox: File version history
Further Reading
Academic Foundations
- Version Control Theory: Zeller, A. (2009). Why Programs Fail: A Guide to Systematic Debugging (2nd ed.). Morgan Kaufmann. ISBN: 978-0123745156
- Diff Algorithms: Myers, E.W. (1986). "An O(ND) Difference Algorithm and Its Variations." Algorithmica 1(2): 251-266.
- Three-Way Merge: Khanna, S., Kunal, K., & Pierce, B.C. (2007). "A Formal Investigation of Diff3." FSTTCS '07.
Practical Implementation
- Git Documentation: https://git-scm.com/doc - Comprehensive version control guide
- diff-match-patch: https://github.com/google/diff-match-patch - Text diffing library by Google
- jsonpatch: https://github.com/Starcounter-Jack/JSON-Patch - JSON document patching (RFC 6902)
- Automerge: https://github.com/automerge/automerge - CRDT-based version control
- Yjs: https://github.com/yjs/yjs - Shared editing with version tracking
Standards & Specifications
- RFC 6902 (JSON Patch): https://tools.ietf.org/html/rfc6902 - JSON document operations
- RFC 7386 (JSON Merge Patch): https://tools.ietf.org/html/rfc7386 - Simplified JSON patching
- WebDAV Versioning: https://tools.ietf.org/html/rfc3253 - Distributed authoring and versioning
Related Trilogy Patterns
- Pattern 23: Audit Trail - Continuous audit vs discrete versions
- Pattern 22: State-Aware Behavior - State transitions create versions
- Pattern 5: Form State Tracking - Track which version user is editing
- Volume 2, Pattern 26: Feedback Loop Implementation - Detect version differences
- Volume 1, Chapter 13: Lessons Learned and Best Practices - Document versioning architecture
Tools & Services
- GitHub: https://github.com/ - Git hosting with pull requests and version visualization
- GitLab: https://about.gitlab.com/ - Complete DevOps platform with version control
- Git LFS: https://git-lfs.github.com/ - Large file versioning
- DVC (Data Version Control): https://dvc.org/ - Version control for ML models and datasets
- Plastic SCM: https://www.plasticscm.com/ - Distributed version control with visualization
Implementation Examples
- Document Version Control Pattern: https://martinfowler.com/articles/patterns-of-distributed-systems/version-vector.html
- Building Version History UI: https://github.blog/2020-12-17-commits-are-snapshots-not-diffs/
- Conflict Resolution Strategies: https://www.atlassian.com/git/tutorials/using-branches/merge-conflicts
- Time Travel Debugging: https://rr-project.org/ - Record and replay program execution