This isn’t one of those “best practice” or “how to” blog posts; the ones where you suspect the author has architected but not actually built a real app for a while. Instead, it’s rather an unglamorous overview of the architecture of UbiquiList.
A few folks asked me which tech choices I made, and a few non-tech friends showed an interest in seeing what goes into an app like this, so I hope this goes some way towards covering both angles.
I suspect that you could build the same web application using 101 different technologies that I haven’t even mentioned here, and combine those technologies in 1,001 unique ways. Some of these would no-doubt be considered much “cooler” than the architecture I describe here. And yet I bet the end result would be uncannily similar from a user’s viewpoint. This is the reason I try to stay out of those almost religious debates about web technology choices. Beyond reading up or comparing notes with others on what’s new or improved, and understanding which choices impact performance, hosting costs or user experience in a tangible manner, I like to spend the majority of my time building things that people actually get to use. Some of my choices here may not match yours. As always, I learned great deal and I will do many things differently next time.
I am staying, out of necessity, at quite a high level here too. I could write an entire post on each of the topics of caching, persistence, performance, load balancing, sticky sessions, AJAX, etc. But this is more of a map than a detailed account. Maybe I’ll cover some areas in detail in another post.
As applications and products go, Ubiquilist is somewhat at the MVP stage, and is therefore lean and very minimal in nature. Some of the architectural decisions I made were for expedience, and relied upon prior work that I could re-use quickly. There is also considerably more “architecture” here than most MVPs would employ, for the same reason: I had it easily to-hand. Most MVPs would skip a lot of this and revisit the need for it later.
Most things are easier to explain if you start with a picture. So here’s a picture…
Single Page rather than Click-Thru – The first thing to point out is that this is an AJAX / single-page web app, rather than what I tend to call a “click-thru” web app. This means that the app dynamically modifies the page content to reflect its internal state, rather than requiring users to click links that cause the server to return an entire new page for each action. Single page apps seem to be the way things are heading.
Kaleidoscope – To build such a single-page app, I used a framework of my own called “Kaleidoscope”. There are several great alternatives out there, such as Knockout, or AngularJS, but I wrote Kaleidoscope (“KS” from now on) for a previous project and so it was the simplest choice for me. I’ve been judged as crazy and brave, in the same breath, for building a framework. It served an educational purpose for me, and the result is something I can build apps with fairly quickly. I probably wouldn’t recommend writing your own framework though. It’s a huge distraction, and there are plenty of existing ones out there!
In a nutshell, KS allows me to quickly build interfaces constructed from a hierarchy of XHTML “fragments” by writing a simple “builder / controller” for each fragment. When requested, a builder provides a JSON model representing some portion of the app state. It also provides an XHTML template for the fragment, which references the model and allows the fragment view to be built by KS. Whenever the model is updated, the builder simply supplies the updated model and the framework handles any changes to the page DOM dictated by the template, whether small tweaks or huge structural changes. There is no need to be concerned with the mapping of one page state to another or updating of the DOM, and KS removes that significant burden from the developer… hence my love of using it, and the reason that fairly dynamic-looking apps can be built so quickly with it.
KS templates may also embed other fragments, allowing the framework to orchestrate those fragments into an overall page view; something else the developer doesn’t need to be concerned with.
Finally, Action tags in templates allow chosen HTML DOM events to be passed back to the builder as application actions, which the builder handles in an app-specific manner, perhaps further updating the model and hence, indirectly, the page. For example, clicking on a button or changing the selection in a drop-down, but also subtler events that may not necessarily change the state of the app directly, such as mouse movements or keyboard events.
Server – All of the app’s server-side components are hosted on a Amazon Web Services (AWS). They now offer such a bewildering variety of components, great scaling options and a year-long “free tier” to get you started cheaply. There are other providers, but I found it easy to get started with Amazon and I know the app can grow without moving from them. Again, another piece I had used before, so I stuck with them.
Hosting Static Content – All images, CSS and the client-side app JS itself are stored on Amazon’s S3 storage service, versioned by build number. When uploading static content to S3 as part of a build, I also set file type and caching metadata using a simple script. In-front of S3 sits CloudFront, which acts as a Content Delivery Network (CDN) and optimises access to that static content via an edge delivery network, depending where an accessing user is located. The app automatically precedes HTML and dynamic references to static resources with the path to CloudFront and my aim was to avoid serving any static content inefficiently from EC2 (see below).
EC2 – All dynamic portions of the app are hosted on one or more EC2 instances, the number of which can be scaled up to meet demand. Those instances sit behind an Elastic Load Balancer (ELB) which automatically forwards requests to “healthy” instances. The ELB is also where the site certificate is installed, allowing secure HTTPS communication between browser and server. In-fact, after the first page hit, UbiquiList mandates a secure connection from then on.
Each EC2 instance is a Linux box running Jetty, upon which the Java server-side application runs. There are other servlet containers, and full-blown application servers available, but Jetty serves the purpose very well for this. My alternative would probably have been Tomcat.
There are also a few bootstrap services, via which the app is launched, or that just don’t fit into the RESTful approach. These accomplish tasks such as handling the first page hit containing the initial skeletal HTML, referencing the correct JS libraries and any environment-specific configuration. They also provide a destination for folks that Kaleidoscope deems to have an old / incompatible browser, and a suitable page is shown to such users explaining why.
Model, Caching, Persistence – The entities that make up a user’s view of UbiquiList (lists, items, cells, other users) are represented by a server-side model. When an entity is accessed via the client, a server-side representation of it is either created or loaded from the database.
To avoid repeated database access, a caching layer is present. The eventual plan is to use Amazon’s ElastiCache to host a shared cache that all EC2 instances access. But, for now, whilst there is a single EC2 instance, an in-memory cache is used instead. This was part of the whole MVP / early version approach, in which scaling decisions are postponed. I do, however, believe in figuring out where scaling might be needed and making sure it can be slotted in later.
MongoDB – Having used MySQL as a database for the past few years, I chose to move to a NoSQL approach this time and went with MongoDB. It isn’t hampered by so many of the scaling issues that traditional relational databases suffer from and, having never used it before, it seemed a great opportunity to give it a whirl. To free myself from the initial learning curve inherent in hosting my own Mongo instance, I chose to go with MongoHQ who provide hosted MongoDB installations. They have options that are hosted within Amazon’s own AWS infrastructure, hence traffic between the EC2 instances and MongoHQ is internal to AWS. So-far, I’m loving the simplicity of both MongoDB and MongoHQ!
Third Party Services – Finally, there are a few third party services upon which UbiquiList relies. MaxMind is used to map a user’s IP address to their geographic location; something that people take for granted with web applications nowadays. I use one of their web services and pay a small fee per 50,000 lookups. This allows UbiquiList to display the location of anonymous / guest users, and helps to assign a basic description to them in the event that they don’t provide a nickname.
Mandrill provides a great email gateway, via which all UbiquiList emails are sent. They support configuration of the appropriate DNS settings, such that emails from Mandrill are known to come authentically from the UbiquiList.com domain. They also monitor undeliverable mails, and provide a whole host of features I’ve yet to explore, such as tracking of email campaigns.
What Else? – I have probably missed out a thousand things, such as Google Analytics, gzip compression, precisely how I upgrade/deploy builds, etc. But I think this is more than enough for an overview.
Where Next? – The architectural decisions I make next for UbiquiList really depend on where it goes as a product. The architecture I describe above is working well as an MVP platform, but I suspect (in-fact, I know) that if the app needs to scale up, I will need to make changes in unexpected places. That’s a great reason for keeping the architecture fairly minimal for now and avoiding components that may never be needed.
I hope this bare-bones description of the architecture of UbiquiList has been useful and, if you’ve reached this far, thanks for wading through it 🙂