Realm Embedded Architecture: Change Notifications, ACID Boundaries, and Failure Handling
An architecture note for Realm Database: using change notifications and transactions to keep mobile UI in sync without polling, while respecting thread, process, and schema boundaries.
29 Jul 2026, 14:23 UTC

The Problem: Synchronizing UI Without Polling
Mobile applications that store data locally often face a choice: refresh the UI on a timer, manually trigger reloads after every write, or maintain a fragile observer layer. Realm Database offers a fourth option: object-level change notifications delivered when a transaction commits. This eliminates polling, but the mechanism is tightly coupled to Realm's transaction and threading model. Understanding those boundaries is the difference between a responsive app and one that crashes intermittently or loses data after a process restart.
Requirements for an Embedded Realm Design
Before selecting Realm's notification and transaction primitives, the architecture must satisfy four constraints:
- Automatic UI updates: views must reflect committed data without explicit refresh calls.
- Atomic writes: a group of writes must either fully succeed or fully fail; partial writes are unacceptable.
- Explicit schema evolution: adding fields or tables requires a versioned migration, not an implicit guess.
- Process resilience: if the operating system kills the app, the on-disk file must remain consistent.
The Smallest Suitable Design
The minimal architecture uses one Realm instance per thread, a change listener registered on that thread, and all writes wrapped in transactions.
Realm instances are thread-confined: an instance created on one thread cannot be used on another. The smallest viable pattern therefore looks like this:
- Open a Realm instance on the thread that will read data (for example, the main/UI thread).
- Register a change listener on that instance.
- Perform writes on a background thread using
executeTransactionorexecuteTransactionAsync. - When the transaction commits, the listener on the reading thread fires, delivering a diff or a change signal.
This avoids polling and keeps the UI thread free of blocking I/O. The transaction boundary acts as the synchronization point: listeners do not fire mid-transaction, only after commit.
Illustrative Listener Configuration
The following Kotlin snippet illustrates the registration pattern on a thread with a Looper (such as the Android main thread). It is an example configuration, not a tested benchmark.
// Main thread (has a Looper)
val realm = Realm.getDefaultInstance()
// Coarse-grained listener: fires after any transaction on this Realm commits
realm.addChangeListener { updatedRealm ->
// Re-query or inspect granular listeners for diffs
refreshUiFrom(updatedRealm)
}
// Background write
realm.executeTransaction { transactionRealm ->
val item = transactionRealm.where(Item::class.java)
.equalTo("id", "placeholder-id")
.findFirst()
item?.status = "completed"
}
// After commit, the listener above runs on the main thread.
For field-level diffs, Realm also provides object and collection change listeners. The architecture should choose the granularity deliberately: a coarse listener is simpler but forces a full re-query; a granular listener reduces work but adds listener bookkeeping.
Trust and Data Boundaries
Realm's guarantees depend on respecting three boundaries. Crossing them silently produces missed callbacks, duplicated updates, or file corruption.
Thread Boundary
Change notifications are delivered on the thread that registered the listener. If you register on the main thread but attempt to read the changed objects from a worker thread without proper synchronization, you may observe stale data or trigger a thread-access exception. The listener callback must marshal data to other threads using thread-safe copies, not shared Realm object references.
Process and File Boundary
A Realm database is a single binary file, often memory-mapped for performance. This gives strong isolation—one file contains the schema and data—but it also means the file is the unit of trust. Abrupt process termination or file-level corruption can render the entire database inaccessible. Multi-process access to the same Realm file requires explicit coordination; concurrent writers from separate processes are not safe by default.
Schema Boundary
Schema changes are gated by a version number. Opening a file with a schema version higher than the code expects, or lower without a migration, throws a RealmFileException. The migration logic in RealmMigration must explicitly bump the version and transform old data. There is no automatic column addition.
Operational Checks
Two tests validate the core assumptions of this design before shipping.
Notification Diff Verification
In an instrumentation or unit test with a Realm instance, register a change listener, execute a transaction that modifies a known field, and assert that the received diff matches the expected change. If the listener fires on the wrong thread or delivers no diff, the threading model is violated.
Schema Mismatch Verification
Attempt to open a Realm file whose schema version does not match the compiled schema and for which no migration is supplied. The expected result is a RealmFileException on open. This confirms that version gating is active and that migration code is required for every schema change.
File Integrity Check
Because the database is a single memory-mapped file, a practical operational check is to verify that backup and restore procedures produce a readable Realm. Copy the file while no transaction is in flight, then open the copy on a clean install. If the copy fails to open, the backup strategy is insufficient.
Failure Modes and Conditions That Change the Design
| Failure Mode | Symptom | Root Cause |
|---|---|---|
| Missed or duplicated callbacks | UI shows stale data or updates twice | Listener registered on one thread, data read on another without synchronization |
RealmFileException on launch | App crashes at startup | Schema version mismatch with no RealmMigration |
| Entire database unreadable | All queries fail after crash | Single-file corruption or incomplete write during abrupt termination |
| Blocked UI thread | Jank or ANR | Large transaction executed synchronously on the main thread |
The design should be revisited under these conditions:
- Multi-process access: if two processes must write to the same Realm file, the single-writer assumption breaks. A client-server or IPC layer becomes necessary.
- Large or streaming blobs: storing large binaries inside the single Realm file inflates backup size and memory-map pressure. External file storage with Realm references may be preferable.
- Server synchronization: if the local Realm must sync with a remote backend, conflict resolution and operation queues add complexity beyond local transactions.
- Frequent schema churn: if the schema changes every release, migration logic becomes a maintenance burden; a more flexible document store or explicit serialization layer may reduce risk.
Practical Verification
After implementing the listener and transaction pattern, run the app, trigger a write from a background thread, and confirm that the UI updates exactly once without a manual refresh. Then force-stop the app during a write and relaunch: the file should open cleanly, and the last committed transaction should be visible. If either check fails, revisit thread confinement or transaction boundaries before adding features.