Carousel’s Design for Speed: Treating Local and Remote Photos the Same
Carousel launched in April 2014 as a single home for all of a user’s photos and videos, regardless of whether those files live on the device or are backed up to Dropbox. Building it meant solving a set of platform and performance challenges, from sharing code between Android and iOS to handling photo collections of over 100,000 items.
A core obstacle was perception: no matter how robust the backend, an app backed by network requests was at risk of feeling slower than a local gallery. The guiding principle for the team was straightforward: a Dropbox-powered gallery should be just as fast as a native one, and it should never force users to wait for an action to complete.
Problems Identified in the Earlier Photos Tab
Before building Carousel, the team took a closer look at the Dropbox app’s existing photos tab and pinpointed two significant technical flaws that hurt the user experience.
First, the tab made blocking HTTPS requests to sync user actions with the server. When a user tried to share or delete a photo, the UI displayed a spinner and waited for a network response. With no connectivity, these requests failed outright, asking the user to try again later.
Second, there was no way to view or interact with photos that only existed locally on the device—that is, photos that had not yet been uploaded to Dropbox.
Combined, these issues made sharing a cumbersome process. The user first had to wait for photos to back up, then wait on another blocking request to complete the share. The application also could not serve as a legitimate replacement for a traditional camera roll, since offline photos were completely inaccessible.
Moving to Percentually Consistent Data
To address the blocking request problem, Carousel’s engineers adopted an eventually consistent system, an approach formally known as optimistic replication. The concept is that a user can perform an action, see the effect of that action locally and immediately, and then have the effect propagate globally across other devices asynchronously.
This design relies on three primary data inputs that are unified and persisted to disk:
- Server state: The client uses HTTP long polling and a delta API to receive notifications of changes and pull them down. Changes like additions, deletions, and metadata modifications are written into a
server_photostable in SQLite. This table acts as a cache of the authoritative server state based on a prior delta cursor. - Local uploads: A client-side camera roll scanner computes a fast hash of every local photo to determine which ones still need a backup. Photos that need to be uploaded are turned into persisted
photo_upload_operationentries. - Pending client actions: User actions such as hide or delete are turned into operations that also persist to SQLite.
Each user action becomes an operation that is eventually synced to the server. Operations are placed in in-memory queues and written to SQLite so they survive app restarts. Dedicated operation sync threads wait until an operation is ready, then make the necessary HTTPS request. When the UI renders a view, it consults these pending operations to ensure it reflects the user's latest impulses.
To unify these inputs and produce a coherent view, cached server content and pending local operations are merged. The data itself is kept loaded in memory rather than forcing frequent fresh reads from SQLite. The model is then modified live as new changes arrive in the form of user inputs or server deltas, an in-memory analogue to the delta-based syncing between client and server.
How Photo Merging Works
Suppose the server_photos table has certain existing photos, the photo_upload_operations includes a new local photo not yet uploaded, and the modification operations include a hide for a specific photo. Unioning the local and server content and then applying the pending hide yields the final visual grid for the user: the local, un-uploaded photo is visible in the same stream as the server photos, and the hidden photo is removed.
This approach works especially well for shared photos that already have server identifiers. However, a question arises when a user wants to apply an action to a photo that hasn’t completed its upload yet. The photo may even be uploaded from a different device or source entirely, not just the current Carousel client. The system must still resolve any pending interactions on that photo after it goes live.
Local Unique Identifiers
To bridge local and remote states, Carousel introduced a concept called a “locally unique ID” (LUID). A LUID is simply an autoincrement integer, but it serves as a stable, canonical way to refer to a photo both before and after it is uploaded to Dropbox.
When the device scanner encounters a new photo that needs to be backed up, a LUID is first created and mapped to its native camera roll ID in a local_photo_luids table. When a new server photo S comes down through the delta, the client checks whether S.hash matches a local photo L. If it does not, a new LUID is forged in a server_photo_luids table mapped to the server ID.
When a hash does match a local photo L, that is a signal that the upload is complete and its server metadata is finally available. The client then assigns S.photo_luid = L.photo_luid and marks the associated photo upload operation as done. To avoid conflicts, the first server photo with a matching hash is the one to claim the LUID.
By using this logic, the app can always refer to a single photo deterministically, saving itself the trouble of tracking down and upgrading each local reference to a new server ID after upload. The UI, the syncing logic, and even user selections establish equality based on LUIDs alone.
Instant Sharing for Mixed Batches
When a user shares a batch of photos containing both local-only items and items already in Dropbox, the selection is based on LUIDs. Even if one of the selected photos completes its upload mid-selection, the selection remains intact.
When the user chooses recipients, the app constructs a corresponding share operation. Rendering the resulting conversation view is simply a matter of reading the cached server state for that conversation and re-playing the pending share operation on top of it, following the same local-first mechanics.
If any LUID inside the share operation is still local only, the share operation queue is not yet ready to contact the server. That queue therefore waits until the dependent photos are uploaded. The dependency is tracked in the share operation’s construction, and the relevant photo_upload_operations are flagged and reprioritized to the front of the upload queue so sharing can happen quickly.
Once the dependent uploads finish, the share can be executed. The server IDs are found via the server_photo_luids lookup, and the request is sent. All of this is asynchronous; the user never endures a progress spinner, even when orchestrating actions across un-uploaded files.
A Lesson in Not Blocking the User
The primary takeaway from this experience is simple: a gallery app backed by a network should not make synchronous calls for user-initiated modifications if it aims for a native, local feel. Through an eventually consistent design, local and remote photos were treated as equal citizens, actionable in tandem at speeds human users can’t perceive as delays. An upcoming installment in this series will cover how the company optimized the latency between disk and memory—crucial for making Carousel feel fluid even as libraries grow past 100,000 photos.



