Implementing Version History in a Mobile App
Imagine a lawyer editing a contract on a tablet, accidentally deleting a paragraph and saving. An hour later – the signing, but the data is gone. Without version history, it's a disaster. We implemented a system that stores every edit and allows reverting to any version in seconds. In projects with legally significant documents, editorial work, or complex tables, every change must be tracked and revertible. How to design a history store that doesn't bloat and works on a mobile device with limited resources? We break down key approaches, their strengths and weaknesses, and provide ready-made schemas and code. Our extensive experience in mobile development has seen us implement versioning in dozens of projects.
Two Approaches: Snapshot vs Event Sourcing
Snapshot — create a full copy of the object on each save. Simple to implement and restore, but leads to explosive database growth. If a document is 50 KB and the user saves it 50 times a day, over a year that accumulates 50 × 50 × 365 ≈ 912 MB for a single document. For an app with thousands of users, this is unacceptable.
Delta (event sourcing) — store only the difference between versions. Compact, but restoration requires replaying all deltas from the initial state. For text, you can use diff-match-patch (free library available on iOS and Android). However, complexity increases with parallel editing.
Hybrid approach — a combination: snapshots every 10 versions or every 7 days, with deltas in between. Restoration: take the nearest snapshot and apply deltas. This method provides up to 90% space savings compared to pure snapshots and speeds up recovery 5–10x relative to pure deltas.
Below is a comparison of the three approaches.
| Characteristic | Snapshot | Delta | Hybrid |
|---|---|---|---|
| Storage volume | Huge | Minimal | Moderate |
| Restoration speed | Instant | Slow | Fast |
| Implementation complexity | Low | High | Moderate |
| Conflict resilience | High | Low | Moderate |
Data Schema
For an SQLite database on Android or CoreData on iOS, we use a table like this:
@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" / "Manual save" ) Index on (documentId, versionNumber) is mandatory — otherwise, fetching history becomes a bottleneck. On Android we use @Index in Room, on iOS a composite index in CoreData.
How to Choose a Version Storage Strategy?
The choice depends on data type and change frequency. For documents with rare edits (1–2 times a day), pure snapshot is simple and reliable. For text editors where changes happen every second, hybrid is better. Always assess the load: if the database grows faster than users create content, change the approach.
Limiting History Depth
Storing all versions forever is not practical. We offer three strategies:
| Strategy | Description | Example Limit |
|---|---|---|
| Fixed count | Keep the last N versions | 50 versions |
| Time window | Versions no older than M days | 30 days |
| Smart thinning | Mixed approach | Full snapshots per day, one per day for older week |
Example of deleting old versions on Android with 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 }) } } Why Limit History Depth?
Without limits, the database can bloat to gigabytes on the device. Users rarely return to versions older than a month. Our experience shows that a reasonable limit of 50 versions or 30 days covers 99% of needs, keeping storage under control. Storage savings up to 90% of DB volume directly reduce infrastructure costs.
Version History UI
A list of versions with date, author, and type (autosave/manual). Tap to preview, two buttons: 'Restore' and 'Compare with current'. A diff-view with highlighting: green for added, red for removed. On mobile we use inline diff via SpannableString (Android) or NSAttributedString (iOS) — compact and intuitive.
Autosave and Debounce
Autosave should not create a version on every keystroke. Debounce of 2–3 seconds after the last change:
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) } } What's Included in the Work
- Data analysis and strategy selection (snapshot / delta / hybrid) — we assess load and guarantee performance.
- Data schema design with indexes and foreign keys.
- Implementation of autosave with debounce.
- Development of version list UI with metadata.
- Creation of diff-view for comparing versions.
- Configuration of background cleanup of old versions on a schedule.
Full Cleanup Example with WorkManager (Android)
class VersionPruneWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val dao = AppDatabase.getInstance(applicationContext).documentVersionDao() dao.pruneAllVersions(keepCount = 50) return Result.success() } } Timeline
Basic implementation (snapshot + simple UI) — 1.5–2 days. Full hybrid with deltas, diff-view, and smart thinning — 4–5 days. Cost is calculated individually based on data complexity and history depth requirements. We deliver turnkey: from design to store publication. Get a consultation — contact us for a project evaluation. Order version history integration — we'll propose the optimal solution.







