Ruby, NodeJs, Go: pros and cons for back-end development
In single-page applications there is a fairly sharp line between front-end and back-end. Presentation and interaction are handled entirely by the front-end, while the back-end takes care of the business logic: it reads the data the front-end sends, processes it and returns the results.
The IT world is constantly evolving: 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. Beyond simple static websites, there has been a growing need for more dynamism, responsiveness and integration with the operating system.
At this point in the Web's evolution, single-page applications seem to be the answer (or compromise) to these needs. A key trait is that the front-end runs in the browser, on the client side (except for the first request in isomorphic applications), while the back-end runs entirely on the server side.
What's up behind the scenes?
The front-end side was covered in depth in the BazarJS series of posts written by our CTO Stefano Verna.
In this article I would instead like to give a (limited) overview of the options available for the back-end, focusing on the aspects I consider essential:
- User happiness: if the application returns correct data, doesn't crash and is fast, the user is happy.
- Client happiness: if the user is happy (and will therefore use the application), and development, maintenance and running (hosting) costs are kept low, the client is happy.
- Developer happiness: if the user and the client are happy, and the codebase is maintainable and easy to change and improve, the developer is happy.
Starting from these points, I would like to set the criteria for comparing the various solutions: language expressiveness, ecosystem and performance.
The contenders
In this article I will look at:
- Ruby (MRI), the language we currently use at Cantiere Creativo for all our server-side projects
- JavaScript (NodeJS), an event-driven framework for the V8 JavaScript engine, which has been very successful in recent years
- Go, a modern language developed by Google, whose adoption has been growing over the last few months
Language expressiveness
In a programming language, expressiveness means how easily and simply you can write an algorithm. Some languages make certain algorithms easier to express through specific patterns.
A more expressive language for solving a given problem makes the developer happier.
Ruby
Ruby is a syntactically rich language (42 keywords versus C's 32, for example), with several built-in data structures (numbers of arbitrary size and precision, strings, lists, maps and sets) and a fairly complete standard library.
One peculiarity of the language is that, since it is easy to metaprogram and has a fairly loose syntax (optional parentheses, postfix forms, etc.), DSLs proliferate and further increase its expressiveness (the flip side is having to remember all the new "keywords" introduced by the various DSLs).
JavaScript
Formally ECMAScript, it is currently the language of choice for client-side web programming. Because of its history it has accumulated a number of bugs and anti-patterns, which should however be fixed in the next language standard (ECMAScript 6, currently in the works). Its syntax is very similar to C's, with comparable expressiveness, though it has higher-level data structures (strings, arrays and hashes) and is object-oriented (prototype-based). Some languages (e.g. CoffeeScript) compile to JavaScript, aiming to hide the language's various quirks and offer higher-level constructs. The asynchronous nature of NodeJS, however, somewhat hurts code readability and correctness, due to the famous "callback hell" problem.
Go
It is a syntactically rather rigid language (very similar to C), with few keywords (25) and a limited set of built-in data structures (numbers of various sizes and precision, strings, arrays, slices, maps and channels). Minimal as it is, it offers good expressiveness, mainly thanks to its distinctive type system, which lets you leverage duck typing. It makes concurrent programming very simple, and memory management is automatic thanks to garbage collection.
Ruby logo
Ecosystem
A language's ecosystem consists of its add-on modules, the community around it and the tools that help produce quality code. Each of these aspects aims to increase productivity:
- add-on modules mean you don't have to reinvent the wheel
- an active community helps you solve problems and doubts faster
- dedicated tools help you make fewer mistakes
Code written in less time — thanks to add-on modules — and with fewer bugs makes the client happier.
Ruby
Although its standard library is quite varied, Ruby also draws on a wide choice of add-on modules, called gems, that solve a broad range of problems. On top of that, it integrates well with various editors and there are some dedicated IDEs.
The community is generally very active and responsive. The biggest drawback I ran into when using Ruby and its gems was the scarcity (in both quantity and quality) of documentation. I often had to read the tests or the code to understand how to use a given gem correctly (or even standard library classes).
JavaScript
With the arrival of NodeJS, an event-driven framework for the V8 JavaScript engine, it became possible to use this language to solve problems outside the browser as well.
NodeJS exposes to JavaScript a whole set of functions for interacting with the filesystem, sockets, etc. Around it, an entire ecosystem of add-on modules has formed, with a concept similar to Ruby gems. It is still a young ecosystem, evolving quickly and with many aspects yet to be standardized.
The NodeJS community is very active too.
Go
One of Go's strengths is a set of tools, some included in the standard distribution and others add-ons, written specifically to integrate easily with editors and IDEs to make code "standard": formatting it and suggesting coding-style improvements, but also automatically removing unreachable code, unused imported modules, etc.
The filesystem layout of Go projects is standard, as is the documentation format. Here too the community is very active, and the consistency that comes from using these tools encourages contributions to open source projects. Documentation is generally satisfactory, but I still sometimes had to read tests and/or source code to understand how a given library works.
JavaScript logo
Performance
Performance as a concept doesn't mean much on its own; it needs context. All languages probably perform similarly on "hello world", but they differ on more complex algorithms, and there will likely be languages and frameworks that perform better on some kinds of tasks and worse on others.
That said, when comparing languages, besides code correctness and maintainability, performance is still worth considering, because it is the first thing users will notice and it separates a good user experience from a bad one.
A fast program makes the user happy.
Ruby
Unfortunately, in its MRI implementation Ruby is rather resource-hungry, especially when using many gems or aggressive dependency-loading strategies (AutoLoad). On top of that there is the global interpreter lock, which prevents parallel code execution even on multi-core systems. So, picturing a Ruby on Rails application, the only way to handle several requests at the same time is to run multiple instances, using even more memory.
JavaScript
Sticking with the web application example, NodeJS can instead handle multiple requests concurrently, since it was implemented using the reactor pattern, entirely with non-blocking calls.
This is still not parallelism, though, and a single blocking call (perhaps in some external library) can compromise the whole application by blocking every request. As with Ruby's MRI implementation, the only way to get true parallelism is to load multiple instances of the application.
Go
In Go things are very different. One of the language's distinctive features is goroutines, functions that can run concurrently with others. Launching one is as simple as a keyword. The Go runtime includes a scheduler that coordinates the execution of an arbitrary number of goroutines over an arbitrary number of system threads (M:N model). This gives fast context switches while using every CPU core. So, in a hypothetical web application written in Go, a single process can keep serving requests even if one of them is performing a blocking operation.
Golang logo
Which one to choose?
Ruby, paired with a web framework, is perfect for prototyping: it lets you get working models in very little time. Unfortunately, though, it has a scalability problem.
NodeJS is a step ahead of Ruby in terms of performance, but it has no comparable frameworks. You also have to consider how easy it is to make mistakes that undermine the application's proper functioning.
Go is certainly less expressive than Ruby and JavaScript, with a young and fast-evolving ecosystem, but it has significant performance advantages.
As usual when comparing languages, there is no one-size-fits-all answer to the question "Which language and framework should I use as the back-end for a single-page application?" Fortunately, though, we have several options and can choose the one that best fits our needs.