Beyond CLEAN and MVP: Architecting an Offline-First Reactive Data Layer in Android
Key Takeaways
- Reactive Data Layer Architecture (RDLA) cuts through the clutter of traditional Clean Architecture by drawing a hard line between what data looks like to the outside world and how it’s actually sourced internally.
- By using Kotlin’s cold Flow streams, developers can escape the limitations of pull-based MVP patterns and create a one-way, reactive data highway straight to the UI.
- For offline-first medical IoT applications, writing user changes to a local queue lets the UI update instantly while network sync happens quietly in the background.
- Offloading network call management to Android’s WorkManager ensures critical data uploads survive even if the user force-closes the app.
- A clever TestExtensions interface lets engineers run isolated unit tests with Robolectric, validating database fallback logic without wrestling with fragile SQLite mocks.
Preface
Mobile apps live in a world of uncertainty. Users want instant loading, offline functionality, real-time updates, and seamless data saving—even when their connection keeps dropping.
While MVP and CLEAN Architecture provide solid foundations for separating concerns, they often create more problems than they solve when applied to the reactive, resource-constrained world of mobile platforms. They either fall short or introduce mountains of unnecessary boilerplate.
This is where Reactive Data Layer Architecture (RDLA) comes in. It’s a practical, mobile-first pattern designed specifically to connect reactive UI frameworks like Jetpack Compose with mobile storage limitations. By aligning these two worlds, RDLA helps developers build robust, offline-first data layers that actually work.
Why Traditional Patterns Struggle
Before we dive into RDLA, let’s look at why conventional approaches often fail in modern Android development.
MVP: The Pull-Based Bottleneck
In classic Model-View-Presenter, communication follows a simple but flawed pattern:
- The Presenter asks the Model for data
- The Model fetches it and returns it via callback
- The Presenter pushes it to the View
This works for simple cases, but falls apart in reactive environments. If a background sync updates the database, the Presenter stays blissfully unaware unless it constantly polls or relies on a complex event bus. MVP simply lacks a built-in way to automatically propagate state changes downstream.
CLEAN Architecture: Mobile Misalignments
CLEAN Architecture excels at keeping business logic independent from frameworks. But when applied to mobile development without adjustments, it introduces two distinct headaches:
The Boilerplate Tax: For simple read operations, CLEAN forces you to create a Use Case class that does nothing but call a Repository method. In database-heavy apps with dozens of tables, this creates a tidal wave of trivial, pass-through classes that add maintenance overhead with zero business value.
Platform Agnosticism vs. Mobile Realities: CLEAN was designed for enterprise backend systems, not mobile apps. It offers no guidance on handling local-remote data synchronization, offline state propagation, or SQLite performance quirks. It’s great in theory but leaves you stranded when dealing with real mobile constraints.
Introducing RDLA
Reactive Data Layer Architecture fills this gap. It’s a pattern specifically designed to connect reactive UI frameworks with mobile storage realities.
RDLA enforces a strict separation between what data is (the public API) and how it’s sourced (the private implementation), operating on three core principles:
- Reactive Push-Based Streams: The UI never pulls data in a one-off request. Instead, it subscribes to continuous streams of data.
- Local Cache as the Single Source of Truth: The UI reads exclusively from the local database. The network exists solely to populate this database.
- Encapsulated Caching & Sync: All logic for checking cache freshness, merging local edits, and triggering background fetches stays hidden inside the Repository.
The Architectural Topology
RDLA splits your data package into three distinct modules: API, Implementation, and Database.
How RDLA Fits with Clean Architecture and MVVM
RDLA isn’t a replacement for MVVM or Clean Architecture. Instead, it’s a specialized, mobile-first implementation of the Data Layer that integrates with them, optimizing the interfaces to eliminate common mobile pain points.
RDLA and Clean Architecture
Clean Architecture’s core rule is that dependencies must point inward toward business logic. RDLA respects this while optimizing for mobile:
- API Module (Entities): Contains pure Kotlin data models and repository interfaces. Zero platform, database, or network dependencies.
- Repository Implementation (Use Cases): Instead of writing a Use Case class for every database read, RDLA lets the presentation layer observe reactive streams directly. For complex business logic spanning multiple domains, you still create standard Use Cases that depend on RDLA’s Repository APIs.
- DataSource Interfaces (Interface Adapters): Private local and remote data source interfaces live in the Implementation Module, acting as boundaries that protect the repository from concrete database and network engines.
- Room DB & Retrofit Client (Frameworks): Concrete implementations live in separate modules. Framework details like Room annotations are entirely encapsulated at this outer boundary.
RDLA and MVVM: Unidirectional Data Flow
In traditional MVVM, the ViewModel actively pulls data and manages its lifecycle. This imperative approach is prone to synchronization bugs.
RDLA transforms this by making the Model a reactive data bus, enabling strict Unidirectional Data Flow:
- ViewModel as a Transformer: Instead of fetching data on demand, the ViewModel observes the repository’s Flow and converts it into a UI-consumable StateFlow.
- Automatic UI Sync: When a background sync updates the database, the database automatically emits the new data. This flows through the repository and ViewModel directly to the UI—no polling required.
- Clear State Separation: RDLA cleanly separates Persistent State (handled via StateFlow backed by Room) from Transient Events (handled via SharedFlow for one-time notifications like connection errors).
RDLA in Action: Health Metric Tracking
Let’s see this architecture in practice by building a data layer for tracking heart rate measurements.
The API Module (Public)
This is the only package visible to the UI layer, containing pure domain models and repository interfaces.
The domain model is a standard Kotlin data class with no database annotations. The repository interface exposes cold Flow streams, defining the public contract.
The Implementation Module (Private)
Everything here is marked internal to prevent leaks to the UI layer.
A cache wrapper manages cache expiration without contaminating domain models with metadata. The repository coordinator handles local and remote data sources, checking cache freshness and triggering background refreshes when needed.
The refresh logic runs in an application-scoped CoroutineScope, ensuring database updates complete even if the user navigates away from the current screen.
The Local Storage Module (Room)
In real-world apps, related features share a database. RDLA introduces Transaction Groups for this purpose—heart rate and blood pressure data might both live under a Cardio Transaction Group, sharing a single Room database instance.
The local data source interface defines read and write operations. Room entities map to domain models, and the implementation includes a critical optimization: distinctUntilChanged() prevents redundant emissions from Room’s table-level observation.
The UI Layer (Compose) Consumption
A reactive data layer needs a presentation layer that can consume it effectively. The UI aggregates data streams into a unified state using StateFlow for persistent states and SharedFlow for transient events.
StateFlow handles continuous states (Connected, Scanning, Disconnected) while SharedFlow with replay cache handles critical one-time alerts—ensuring users don’t miss important notifications even during orientation changes.
Designing for Offline Mutations
Mutations (user-initiated updates) are processed either synchronously or asynchronously based on user experience requirements.
Synchronous Mutations
These require the user to be online. If the network request fails, the local database remains unchanged and the user immediately sees an error. Think of deleting medical logs—you want confirmation before committing.
Asynchronous Mutations (Offline-First)
For standard logs like recording heart rate during a workout, the write must succeed immediately even offline. The mutation goes into a local database queue, merges into the active UI data flow on the fly, and syncs in the background.
The mutation merger in the local data source combines saved records with pending mutations, overlaying local changes on top of the persisted data.
RDLA splits background execution between appScope Coroutines and Android WorkManager. Lightweight local database writes run in appScope, completing even if the user navigates away quickly. But for asynchronous mutations bridging local database to cloud—or pushing large firmware payloads over BLE—appScope isn’t enough. Android’s Doze mode and process death can terminate the app mid-flight.
By enqueuing mutations via WorkManager, the sync request is delegated to the OS system service, ensuring critical payloads execute with proper constraints (like unmetered Wi-Fi or sufficient battery) and resume reliably if connections are severed.
Conflict Resolution and Rollbacks
Optimistic local updates provide seamless UX but risk collisions. When the sync worker uploads to the server, it might face rejection—an HTTP 409 Conflict if another device modified the record, or 422 Unprocessable Entity if validation fails.
RDLA handles these gracefully. The worker catches failures and flags the local mutation as FAILED instead of retrying endlessly. The repository emits this failure as a transient event to the UI via SharedFlow. Simultaneously, the local data source purges the rejected mutation from the queue. Because the UI is reactively collecting the merged flow, purging triggers a new emission, and the UI instantly reverts to the last known, server-confirmed state.
Why RDLA Makes Testing a Breeze
One of RDLA’s greatest strengths is simplified unit testing. By isolating Room and Retrofit configurations, you can test repository logic directly.
The TestExtensions Pattern
Since repository interfaces shouldn’t expose insertion methods to clients (preventing the UI from messing with sync state), we introduce a test-only interface within the API target.
- Define the interface in the API module with test visibility
- Implement it in the repository class
- Write decoupled unit tests using Robolectric with real Room databases
Core Testing Benefits:
- No SQLite Mocking: Using Robolectric with a real Room database validates SQL queries at test time
- Robust Offline Verification: Inject a fake remote data source to simulate network failures and test fallback logic
- Decoupled Refactoring: Schema changes won’t break repository tests as long as mapping logic translates entities correctly
Conclusion
Building a responsive, offline-first Android app requires a data layer designed for reactivity from the ground up. By applying RDLA, you establish a clean boundary between public data contracts and private, framework-specific implementations. Your presentation layer operates purely reactively, observing data changes rather than procedurally querying them.
RDLA simplifies testing by encouraging interface-based programming and clean seeding patterns like TestExtensions. Ultimately, it provides the structure needed to scale cleanly, handle synchronization challenges gracefully, and deliver the rich, offline-first experiences modern users demand.
BinaryPilot
July 13, 2026The TestExtensions pattern is a hidden gem. Testing repository logic without mocking SQLite is a massive productivity boost. Having to implement a test-only interface might feel like cheating the architecture, but it’s a practical trade-off that dramatically improves test reliability and developer confidence.
vcdgyo
September 25, 2026The offline-first approach is refreshingly pragmatic. Rather than treating offline as an edge case, RDLA makes it the default. The asynchronous mutation queue pattern elegantly solves the age-old “write locally, sync later” problem without forcing developers into complex state management gymnastics.