Designing Web Applications for Scale
Web applications have long suffered from poor design habits. The rise of web applications came from accessible shared hosting environments and the rise of programming languages like PHP. Using FTP to transfer your application source and using web-based frontends to manage and service databases became the de facto method of deploying web applications. It promoted developers to never really understand how the cogs fit together.
The Web is our democratic information sharing platform. It allows us to create device-neutral interfaces for information warehouses large or small. Applications on the Web must thus be designed well and since the entire world is watching they must be designed to scale.
At Anomaly we design web platforms that compliment our customers' businesses. These applications are highly critical and thousands of users depend on them everyday. We've always been about adopting good software engineering habits. Here are things we do to build highly scalable web applications.
Our applications are written in Python, but these habits should be transferable to all properly designed programming languages.
A Brief Note on the State of Infrastructure
I started out building software for the Web in the early 2000s. Infrastructure was a different beast. It was expensive and mysterious. Over the years it became more accessible and at a certain point our business managed and serviced a number of dedicated servers for our customers.
Fast forwards a few years; these services have turned into inexpensive—for what's on offer—platforms. With the coming of application-level virtualisation (e.g. Docker Containers, Google App Engine, Amazon Elastic Beanstalk) it's crucial to design applications to be first class citizens and be ready to scale on demand.
The first step to thinking of your application as a first class citizen. Often because only a closed group of engineers are responsible for deploying your applications, they aren't packaged properly. This leads to you preparing your server environments manually, followed by installation and testing of your application.
If you promote your application to a first class status and employ the use of an installer (in our case, pip) you automate deployment of your own application thus vastly reducing the chance of human error.
It ensures that you can automate upgrades from one version to another. Thinking of your application as a container is the first step towards scalability. Your application can quickly be installed on multiple load balanced nodes and eventually be adapted to automatically scale on a PaaS. We tend employ the use of application-level containers like Python's virtualenv if we deploy to Linux virtual machines.
Detach Storage from Application Servers
Poorly designed web applications are notorious for writing user uploaded content to the server file system. The database backend often resides on the same host. Apart from its obvious issues with performance (increased disk IO on VMs) it creates a single point of failure.
Designing for scale requires that you relinquish your application servers of any responsibilities of storing content. You should be able to commission or decommission an application server without effecting the availability of content created by your application.
This is rather simple to achieve by using storage services like Amazon S3, Google Cloud Storage, etc. Difficulty of separating database backends will depend on the platform where you wish to deploy your application. If you're happy to buy into a PaaS provider, each one offers several managed backends.
Defer Jobs that can Wait
Computing is the new expensive commodity. Storage is cheap. Web applications seem to want to do a lot in their request life cycle. Consider a typical scenario where your customer hits a 'place order' button. Typically designed web applications will attempt to process payment, save the order, generate PDF invoices, send confirmation emails and update order totals all in the one request. Poor system administration practices increase request timeouts to ensure your code can perform those tasks in a single request life cycle. This is a stop gap approach.
Not everything your application does has to be instant. Design your application so you can defer anything that can wait. User-facing endpoints (APIs or generated pages) must only service what's immediately required; everything else should be written to a queue and be processed by background job processors.
Process the payment, update the totals, but queue the invoice generation and email notifications. Approaching your application in this manner will strengthen security—not every node will be authorised to send emails or contain code that produces PDFs and vice-versa—and your background processors won't contain API or page generation code.
RDBMS's (particularly MySQL) have been the default choice for web application backends. While they are suited to many use cases, they are particularly bad for others e.g. content management systems. Your application can greatly benefit from a NoSQL database. For many use cases on the Web they outperform relational databases as your application needs to scale.
After years of thinking in relational ways, developers find it difficult to think in the NoSQL way. It takes little a side step in thinking but your application stands to greatly benefit from the move.
This is no way a statement against RDBMS or relational design. It's a statement on being fit for purpose.
All applications perform best when treated as a first class citizen of their environment. Native mobile applications perform best when you use prescribed APIs and techniques. Similarly, web applications perform best when they are built as a first class citizen of the environment they are going to reside on.
And picking the right ingredients is part of the secret to a stellar application.