The Two ID Problem

I want to call out a surprisingly tricky problem we ran into when developing offline mode: handling identifiers.

In Trello, all models have an ID. Each board, list, card, etc. It’s how we define relationships between models as well as how we communicate with the server about them.

When you’re online-only, you can depend on the server to provide all your model IDs. But when you’re offline, the client will have to create new models without the aid of a server-defined ID.

Essentially, an offline solution must involve two IDs: one that can be created/used locally, and one that can be used for communication with the server. It’s a seemingly simple setup that created many headaches for us.

This problem is further complicated by the relationships between models. Not only does each model in the database have an ID, but it may list both child and parent IDs, pointing to other models. Whatever identifier solution we come up with will have to maintain those relationships.

False Starts

There were a few bad paths we went down initially.

The first solution we tried was generating local IDs offline, then switching to the server ID once the model was synced. An obvious idea, but unfortunately lead to poor performance due to IDs being used to define relationships between models. We wouldn’t just be updating the ID on the model - we’d have to hunt down and update IDs on all the affected models. Not only was this slow, but it was also rather fragile. It would be extremely easy to forget to update one ID and have the whole app fall over.

The second attempt was to store all IDs as a pair, then have a database table mapping local <-> server IDs. While it would work, this solution would require a huge refactor of our codebase. Every place we used a String for IDs would have to be replaced by this new Identifier class. This proved to be an insurmountable amount of code to refactor safely.

Additionally, there was no guarantee that any given in-memory Identifier would have both a local and server ID, so we’d have to constantly be checking in with the database to fill out the data for any given identifier. This process would add tons of performance hits and boilerplate code.

I wish I could say that we figured out ahead of time that these routes would lead to disaster, but the truth is that we wasted a lot of time determining how infeasible they were.

The Local-Server Barrier

Ultimately, we came upon a solution that worked well that I like to call the local-server barrier.

The key to it is that, within the confines of the app, we would use only local IDs. Whenever we communicate with the server, we’d convert to/from server IDs as necessary.

ID barrier diagram

There were a couple major advantages of this solution:

  1. It keeps the code on the client simple. Only the networking layer has to be concerned with server IDs; the rest of the code doesn’t even care.
  2. It required the least amount of refactoring. Most of our code could operate exactly as before; only the networking layer needed modification.

This solution did borrow from previous attempts; for example, we keep a mapping table from local <-> server IDs that the barrier could use.

ID Converter

The local-server barrier required a lot of conversion of IDs. We’d get models from the server with all server IDs, then we would need to convert it the same object but with local IDs.

Our solution was to write an annotation-based ID converter. You’d simply annotate fields which are IDs and then it could run through the model and rewrite those fields with local IDs.

public class Card {
    private String id;

    private List<String> memberIds;

This method saved a lot of time for initial development and still works fine today. Unfortunately, this solution means that our data must be mutable. We’ve been moving towards immutable value types (since it simplifies code) and thus we may end up shifting to a different solution sometime in the future.

This article was originally posted on the Trello engineering blog and has been reproduced here for posterity.