Engineers have been shuddering at the words “how hard could it be?” for hundreds and hundreds of years. After all, for every great engineering triumph, there are a thousand small failures along the way. The same is true for mobile development, where you’ll often hear this variation of the same question: “How hard could it be to make an app work offline?”

What is it about a closing elevator door that makes an app so useless? It couldn’t be that users simply don’t want or need offline support. After all, the user doesn’t disappear when a network connection does. They’re still there, waiting patiently for a spinning wheel of boredom to disappear, ready with their thumbs to do the work they rely on the app to perform.

It’s not like the data necessarily has to disappear, either. If your cell phone connection runs out and you accidentally close your mapping app, why should the directions you loaded disappear? The app already made the network request, and the server already responded; you’ve probably already started following the road. The data was there and useful already — it’s probably even more useful now that you can’t connect to the back end — so why do so many apps act like the data you were just using never existed in the first place?

Why is it so rare to find an app that works just as well offline as it does online? Because it’s hard. And hard in somewhat surprising ways, even for talented, experienced mobile engineers.

When you first sit down to add offline capabilities, you think it just means adding persistence. That’s easy! Databases and ORMs are familiar tools, and while they may need the occasional performance tweak, they’re well-solved pieces of technology. So, you might reason, you can just save the results of new network requests to the app’s local database, and load whatever local data you’ll need if you can’t get online. Done!

This sounds great, until you run through the list of views and network requests and data types that your app uses. And then you multiply that code by two: you’ll need to write code to manage offline states every time your users get data from the server, as well as every time your users need to add data to the server. That sounds like a lot of change — and your work isn’t done yet. Any time you add new functionality to your app, offline capabilities remain an extra burden to add to your feature list.

What’s more, persistence alone isn’t going to solve your real problem. You’re trying to make an app that works just as well offline as it does online. It lets you do everything that doesn’t require an active network connection. And persistence is a small part of that story.

An offline-first app is different than an online-only app. Making the shift requires a fundamental rearchitecting: you need your app to react to data. You’re going to change your app’s architecture to be built around your database, instead of built around requests and connections to your server. With a modern mobile database at the core of your app, you get a predictable way to manage the flow and creation of data so that it can be queued for networking, quickly persisted, and consistently accessed even without a network connection.

So what does this world look like? It starts by treating the database as a clearinghouse for all data, so that your app’s various systems will react as data changes within the database. When a network request or a user action creates a new piece of data, it puts that data in the database, which then sends out the message that there’s new data to process, display, or send.

By moving to this reactive architecture, your UI gets some pretty enormous benefits. Instead of requiring that you call your UI refresh code everywhere that a user might take an action, you simply subscribe to the updates that your database sends out when it gets new data. It keeps your code simpler, and your UI feels more responsive.

It’s also a much saner way to perform the networking that your app needs. Eventually, anything a user does will have to get sent to the server. But if you’re not sure if you’ll have a network connection, and you are sure you’ll need to support offline states, then it’s best to translate user actions into persisted data first. That way, the database can let your networking layer know that it has some requests to make, and your app’s networking layer can make the right call about when to upload that data. Instead of spreading networking methods throughout your app, constantly re-writing error handling code, and jankily implementing offline states, you can clearly separate networking, data persistence, and UI code. It’s a relief.

Suddenly, you don’t just have an offline-first app. You have an app that is architected so that it can roll with changes, whether they be user actions, networking problems, or new features that you’re building. You’ve got a foundation for doing great work, and you got there by delivering an even better app to your users — wherever they might want to use it.