The IT world is in continual evolution. Every day new languages, libraries and programming patterns are born and die before our eyes. The Web, in particular, has gone through dramatic changes in recent years. There has been a step by step movement away from simple static websites towards more dynamic, responsive websites and increased integration with operating systems.
At this moment in the evolution of the Web, single-page applications seem to be the choice (or compromise) that best answers these requirements. One important characteristic of these applications is that the frontend is exectuted by the browser, on the client side (except for in the case of isomorphic applications), while the backend is handled entirely on the server side.
The frontend component of this arrangement has been analysed extensively by our CTO Stefano Verna in his series of posts entitled BazarJS.
In this article I would instead like to give a (limited) overview of the different alternatives currently available to develop the backend, concentrating on what I think to be the most fundamental aspects:
- User satisfaction: if the application provides the correct data, doesn’t crash and works quickly, the user is happy.
- Client satisfaction: if the user is happy (and therefore uses the application) and the costs of development, maintenance and functioning (hosting) are kept in check, the client is happy.
- Programmer satisfaction: if the user and the client are happy and the codebase is maintainable and easy to modify and improve, the programmer is happy.
Taking these measures as a starting point, I would like to compare the different solutions available under the following headings: language expressiveness, ecosystem and performance.
In this article I will be looking at:
- Ruby (MRI), the language we currently use in LeanPanda for all of our server side projects
- Go, a modern language developed by Google whose use has become increasingly widespread in the past few months
Expressiveness, for a programming language, refers to the ease and simplicity with which it allows one to write an algorithm. Some languages make it simpler to write certain algorithms by using particular patterns.
The more expressive a language is for solving a given problem, the happier the programmer is.
Ruby is a language that is quite syntactically rich (for example, it has 42 keywords while C has only 32). It also has a variety of built-in data structures (orders of magnitude and arbitrary precision, strings, lists, maps and sets) and a reasonably complete standard library.
One of the language’s peculiarities is that, thanks to the simplicity of its being metaprogrammed and having quite a free syntax (optional parentheses, postfix forms, etc.) there is a proliferation of DSLs that increase its expressiveness (the downside of this is having to remember all of the new “keywords” introduced by the various DSLs).
These languages make it possible to construct higher level code. However, the asynchronous nature of NodeJS does impact negatively on the legibility and accuracy of the code, giving rise to the infamous “callback hell” problem.
Go is a rather syntactically rigid language (similarly to C), with few keywords (25) and a limited set of in-built data structures (orders of magnitude and precision, strings, arrays, slices, maps and channels). Despite this essentialism, the language boasts a high level of expressiveness, thanks in particular to its type management, which make it possible to take advantage of a concept known as duck typing. The language greatly facilitates concurrent programming. It also has a system of automatic garbage collection that makes memory management easy.
A language’s ecosystem is made up of the additional modules available for it, the community that makes use of the language and the tools available that can be used to help to produce quality code. Each one of these aspects increase productivity:
additional modules mean that you don’t need to waste time reinventing the wheel
an active community makes it possible to resolve problems and/or queries quickly
dedicated tools make it easier to avoid making errors
Code that takes less time to write — thanks to additional modules — and has fewer bugs makes clients happier.
In addition to having a large standard library, Ruby makes use of a vast selection of additional modules. These are known as “gems”, and they make it possible to resolve a wide range of problems. The use of gems is well integrated with a number of dedicated IDEs.
The community is generally very active and responsive. The biggest flaw I have come across while using Ruby and its various gems is the scarcity and low quality of documentation. I often find myself having to resort to reading through tests or the code itself in order to understand how to use a particular gem (or even a class in the standard library) correctly.
There is also a very active community surrounding NodeJS.
One of Go’s strong points is the presence of a series of tools, some of them additional and others included in the standard distribution, designed to make the code more “standard”. These tools are specifically written in order to be easily integrated with IDEs and editors. They format the code, give suggestions about coding style and automatically eliminate unexecutable code, unused imported modules, etc.
The layout of the file system for Go projects is standard, as is the format of documentation. The Go community is also very active, and the homogeneity resulting from the use of these tools encourages contributions to opensource projects. The documentation is generally satisfactory, but I have nevertheless sometimes found myself having to read through tests and/or source code in order to find out how a particular data library works.
The meaning of “performance” is context dependent. All programming languages will probably perform similarly when executing a “hello world” programme, but will distinguish themselves from one another when it comes to executing more complex algorithms. Some languages and frameworks have better performance than others when it comes to performing particular tasks.
This said, performance is — in addition to code accuracy and maintainability — an important aspect to be taken into consideration when deciding which language to use, given that it’s one of the first things that will jump out to the eye of the user and will differentiate a good user experience from a bad one. The quicker the programme functions, the happier the user will be.
The MRI implementation of Ruby is quite resource hungry, above all when multiple gems or when massive dependency loading (Autoload) strategies are being used. To this limitation it is also necessary to add the presence of the global interpreter lock, which prevents parallel code execution even on multi-core system. As things stand, the only way of managing multiple, simultaneous requests using Ruby on Rails is by using multiple instances, which eat up additional memory.
Sticking with the web application example, NodeJS is — unlike Ruby — capable of managing multiple concurrent requests, taking advantage of the fact that it has been implemented using the pattern reactor and its calls are entirely unblocked.
That said, this is not parallelism and a single blocked call (occurring, for example, in an external library) can interrupt the functioning of the entire application by blocking all requests. As in the case of the MRI implementation of Ruby, the only way to achieve true parallelism is by loading multiple instances of the application.
In Go things are different. One of the language’s peculiarities is the presence of goroutines, functions that can be executed concurrently with one another.
These can be launched simply by using a keyword. Go runtime contains a scheduler that coordinates the execution of an arbitrary number of goroutines on an arbitrary number of system threads (the M:N model). In this way it is possible to carry out rapid context switches in order to take advantage of all CPU cores. So, in a hypothetical web application written in Go a single process will be able to continue serving requests even if one of these is trying to execute a blocked operation.
Which to choose?
Ruby, accompanied by a web framework, is perfect for prototyping, as working models can be built extremely quickly. Unfortunately, it suffers from scalability problems.
NodeJS is a step ahead of Ruby in terms of performance, but it doesn’t possess comparable frameworks. It’s also worth taking into consideration the ease with which it’s possible to commit errors that compromise the good functioning of an application.
As usual when dealing with languages, there isn’t a single answer to the question “Which language and framework should I use for a single-page application?” that will work for everyone. Fortunately, we have multiple alternatives available to us, and can choose the one which is best adapted to our particular needs.