SPAs Are Just Harder, and Always Will Be

There's quite a lot of talk these days about Single Page Applications (SPAs) and the terrific user experience they deliver. This is great, and the SPA architecture is probably a good fit for many apps, but I want to point out two reasons why SPAs are harder to develop and maintain than traditional server-side web apps, and why they will always be harder regardless of new JavaScript frameworks and other technologies that become available. If you're considering a SPA architecture, remember to weigh this additional difficulty against the user-experience benefits you'll get. For the typical line-of-business app full of big forms and complex data the traditional choice could very well be the right choice.

First Essential Difficulty: An Additional Logic Tier

In all but the most trivial apps, if you build a SPA, you'll need three tiers of logic instead of two. I'm defining a logic tier as a physical tier that contains "business logic" specific to the features of your app. In a traditional web app, regardless of your web server platform, MVC framework, etc., if you want to add a customer editor (for example) to your app, you need to add some database tables (data tier) and some code that builds HTML and handles form submissions when users make changes (web server tier). In a SPA, you'll obviously still have a data tier, you'll have a web server tier that exposes the data via JSON or XML web services, and finally you'll have a browser tier with some JavaScript-based code that presents the data to users and handles updates.

Let's consider the code for a simple, read-only display that shows a customer and some orders.

Traditional Web App

On the web server:

var customer = retrieveCustomerFromDatabase();
addCustomerToHtml( customer );

var orders = retrieveOrdersFromDatabase( customer );
addOrdersToHtml( orders );

SPA Equivalent

On the web server:

var customer = retrieveCustomerFromDatabase();
addCustomerToJson( customer );

var orders = retrieveOrdersFromDatabase( customer );
addOrdersToJson( orders );

And in the browser:

var customerAndOrders = retrieveCustomerAndOrdersFromWebService();
addCustomerToHtml( customerAndOrders.Customer );
addOrdersToHtml( customerAndOrders.Orders );

With the SPA architecture, you essentially have to deal with the customer and the orders twice. It's not that you have to do any of the mechanical tasks twice: you're still only retrieving the data once and building the HTML representation once. But you must now publicly present your customer and orders from both the web server tier and the browser tier. If you add a new type of data to your customer display, such as a list of shipping addresses, you need to add the addresses to the JSON and the HTML. If you need to restrict access to a field (e.g. social security number) such that only certain users can see it, you need the authorization check twice, once when writing the JSON and again when building the HTML. In a traditional web app, you can go straight from the data to the final HTML representation with a single block of code on the web server. Here's a diagram that sums up the difference:


In simple cases, you may not need to write any logic for the web server tier, opting instead to use something like OData or a "backend as a service" offering such as Windows Azure Mobile Services. But as soon as your customer editor makes first contact with users, and they start making requests, and you start embellishing the screens with various bits of information from other database tables or data sources, you'll need your own web services to bundle things up and avoid a very chatty conversation between web browser and server. There will be no silver-bullet solution to this issue since network latency isn't going away anytime soon.

Second Essential Difficulty: Data Synchronization

SPAs typically store a larger amount of data in the browser tier at any given time; this is how they enable users to move from task to task without delay. Data in the browser always has a risk of going stale due to external changes, and the more of it you have, the harder it is to determine during synchronization whether it is or isn't stale. And when it does go stale, the more data you have, the harder it is for you (or your users) to reconcile it. Back to the customer editor example: what happens if a user edits a customer, navigates, and begins editing a second customer before you detect that both records were modified a few seconds earlier by a background process that is cleaning up customer mailing addresses? You may be able to provide good behavior to handle this, but it's not easy, and it will involve logic specific to the situation. And issues like this are exacerbated if you decide to support offline editing. The more time you allow between data synchronizations, the more problems you'll have.

In a traditional web app, modifiable data in the browser tier is limited to a single set of HTML form field values and that's all you ever need to synchronize. Concurrency can never cost the user more than one set of field values. In our example, if the user edits a customer record and submits, the browser window blocks the user from moving to the next task until the modified record is reconciled. Is this a lower quality user experience? Yes. But it's also a much simpler concurrency model.


Think About It

Nobody can argue that SPAs provide a more continuous, fluid user experience. But if you're building an app that honestly is only going to serve 10 people, or 100---or 1000---do you really want to incur higher development and maintenance costs just for the sake of pushing the user-experience envelope?


Thanks to Sam Rueby and Greg Smalter for reading drafts of this.