This is something that I had wanted to try in my other apps, but I never could find a way to do it properly. It was really a side-note, a cool feature, perhaps, but for my latest app this feature was going to be a requirement, a part of the "minimum viable product" feature-set. So before even starting to develop Accountant I had to plan out how I was going to achieve this. After spending some time thinking about it, the first question I set out to answer was the one in the title.
You might say the answer is obvious, but in this article I'll try to reveal the nuance of the problem.
In an ideal world offline capability means that your normally cloud-based application can also fully work offline. All of the data is in some way cached on the client side to allow for this. Moreover, everything that you do whilst offline is synchronized with the cloud immediately after you come back online so that the cloud and local data remain in sync. Now, in a not-anymore ideal world (at least from a developer's perspective) let's say someone uses your application on multiple devices and in an unfortunate turn of events they make conflicting actions on different devices whilst one of those devices is offline. What happens when that device goes online and the conflict materializes?
Well there are many ways to approach this. You could:
- Inform the user of the conflict and have them decide how to resolve it. This is a terrible idea from a business perspective and I explain why further along.
- Apply only the action that first reached the server
- Apply only the action that last reached the server
- Apply only the action that occurred most recently regardless of when it reached the server
So instead of speculating on which is the best approach I decided to see how other more experienced developers handled the situation. My go-to app for this was Wunderlist (currently Microsoft To Do). It was my favorite to-do app and was the basis for my own To Do Assistant. Wunderlist was a native Android (and probably iOS) app with offline capability, but it also had a web version. So I used it to examine a sync conflict in the following manner:
- I changed the name of an existing to-do in the web version on my computer (while being online)
- From my phone, while being offline I changed the name of the same to-do to something different
- Then I went online on my phone
Can you guess what happened?
The change that I did on my phone was applied. That sounds like a reasonable approach, right? The change the happened last was applied. But that's not the whole story. I decided to verify my assumption so in my next test I did this:
- From my phone, while being offline I changed the name of an existing to-do
- Afterwards, on my computer (whilst online) I changed the name of the same to-do to something different
- Then I went online on my phone
Guess what, the change that was applied was again the one on my phone. So it seems to be the case that Wunderlist didn't care when the action happened, it just applied whatever action last reached the server. In other words, it didn't seem like it was doing synchronization at all. It just allowed possibly conflicting changes to be applied to the server in the manner in which they reached it. Wunderlist was taking the easiest and least data-safe approach of all.
Does this make Wunderlist a bad app?
According to its download count and reviews not really. The developers chose that approach because in a to-do application perfect synchronization just didn't matter that much. The acceptable safety level of the offline capability was a business decision. This leads me to my main point.
Offline capability is not binary, it's a spectrum. And where on that spectrum your application will find itself is dependent on its business model.
Let's look at another example, a banking mobile app. If you were to have offline capability in such an app you could perhaps get it in the form of viewing your transaction history. But when the time came for you to actually make a transaction you would have to go online because you can't make a monetary transaction without sending something out to the bank's servers and having it verified. So can we really blame the banks for not creating mobile apps that are fully offline capable? Complete offline capability is just not possible in their business model.
The flip side of that example is Git. It's completely distributed yet it comes with the guarantee that you can keep both versions in sync with almost zero risk of data loss. And we all know how Git achieves this. If a conflict happens that Git cannot automatically resolve, it informs us of this and doesn't let us sync (merge) until we have resolved the conflict ourselves. This is reasonable approach for Git because it's whole business model is based on your code history being consistent and safe from synchronization issues. Resolving conflicts is not a great user experience, but with source control data consistency matters more so we accept it. And whilst that may be the case for Git, it isn't for most apps out there.
Wunderlist chose not to bother their users with conflict issues because they know it will just scare the users away. When your average Joe uses a to-do app they don't want to be bothered with implementation details and if they see a conflict the first thing they'll think of won't be "ohh great, this is so thoughtful for safeguarding data consistency". They will think "ohh what did I do now, I broke the app" and possibly abandon it for a competitor's app that doesn't seem to be having these synchronization issues. So Wunderlist chose not to bother their users with it. And the approach they went with wasn't any better or worse than the other two approaches for automatically resolving conflicts. They are all equally ineffective at safeguarding data consistency.
So I concluded that there's only one thing that I can do to protect my users from themselves - make it difficult for them to cause conflicts in the first place.
I made it so that every time someone uses Accountant whilst offline they are not allowed to modify any of the data that has already been synced to the cloud. This might sound very prohibitive at first but if you know how the average person uses Accountant it will make a lot of sense. If a user is offline, they will be allowed to view existing data, enter new data, and update or delete any of the data they created in the current offline "session". This reduces the chances of conflict significantly and covers 99% of the app's use cases. When a user is offline and views some data that's already been synced to the cloud they see a warning message notifying them that they cannot modify it unless they connect to the internet, and any buttons like "Save" or "Delete" are disabled in the UI. So in a way, the app is guiding its users into a safer way of going about their offline business. Once the user comes back online the app syncs with the server and applies all of the changes both ways.
Although my approach is still not perfectly consistent, it is as close to consistent as one can reasonably expect.
Offline capability is about making compromises. Let your business requirements decide what will be compromised to achieve it.