Implementing Version History in Mobile Applications
Version history is not just a log. It's the ability to revert to a previous document version, compare changes, and understand who changed what and when. For documents, spreadsheets, notes, or settings, this can be a critically important feature. Improper implementation leads to either explosive database growth or data loss during conflicts.
Two Approaches: Snapshot vs Event Sourcing
Snapshot: With each save, create a full copy of the object. Simple to implement, easy to restore. Problem — storage size. If a document is 50 KB and the user edits it 50 times a day — 50 × 50 KB = 2.5 MB per day for a single document.
Event sourcing / Delta: Save only the diff between versions. Compact, but more complex to restore an arbitrary version — you need to replay all deltas from the initial state. For text content — unified diff via DiffMatchPatch (available on Android and iOS as a port of Google's diff-match-patch).
For most mobile applications — hybrid approach: snapshot every N versions or every M days, deltas between them. Restoration: take the nearest snapshot, apply deltas.
Data Schema
@Entity(tableName = "document_versions")
data class DocumentVersion(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val documentId: String,
val versionNumber: Int,
val deltaJson: String?, // null if snapshot
val snapshotJson: String?, // null if delta
val authorId: String,
val deviceId: String,
val createdAt: Long = System.currentTimeMillis(),
val comment: String? = null // "Autosave" / "Manually saved"
)
Index by (documentId, versionNumber) — mandatory for fast history retrieval of a specific document.
Limiting History Depth
Storing all versions forever is impractical. Strategies:
- Fixed count: Store last N versions (e.g., 50). Old ones deleted by scheduled job.
-
Time window: Versions from last 30 days. Via
WorkManager(Android) /BGProcessingTask(iOS) daily, remove outdated ones. - Smart thinning: Full versions for today, one per day for last week, one per week for last month.
// Android — delete old versions via WorkManager
@Transaction
suspend fun pruneVersions(documentId: String, keepCount: Int) {
val versions = getVersionsByDocument(documentId)
if (versions.size > keepCount) {
val toDelete = versions.drop(keepCount)
deleteVersions(toDelete.map { it.id })
}
}
Version History UI
List of versions with date, author, device, and type (autosave / manual). Tap version — preview. Two buttons: "Restore" and "Compare with current".
Version comparison — diff-view with highlighting additions (green) and deletions (red). On mobile, typically side-by-side or inline diff. Inline is simpler for small screens: standard approach with SpannableString (Android) / NSAttributedString (iOS), colored insertions and strikethroughs.
Autosave and Debouncing
Autosave shouldn't create a version on every keystroke. Debounce 2–3 seconds after last change:
// iOS — autosave debouncing
private var saveTask: Task<Void, Never>?
func textDidChange(_ text: String) {
saveTask?.cancel()
saveTask = Task {
try? await Task.sleep(nanoseconds: 2_000_000_000)
guard !Task.isCancelled else { return }
await saveVersion(text, type: .auto)
}
}
Work Scope
- Choice of strategy (snapshot / delta / hybrid) based on data volume
- Data schema with indexes and foreign keys
- Autosave debouncing
- Version list UI with metadata
- Diff-view for version comparison
- Cleanup of outdated versions on schedule
Timeline
Snapshot history with basic UI: 1.5–2 days. Hybrid with deltas, diff-view and smart thinning: 4–5 days. Cost depends on stored data format and history depth requirements.







