Building Ruby on Rails from Scratch, Day 3

Pros and Cons of Rails

Chunting Wu
8 min readAug 1, 2022
https://dev.to/rly

This is the last article in this series. Instead of building an application from scratch, in this article I’d like to talk about some of my thoughts on Ruby on Rails over the past few weeks.

First, I’ll start with a brief ranking of the programming languages I’ve experienced on multiple aspects, including C++, Python, Node, Golang, and Rails, and since I haven’t worked with Java or .NET Core, perhaps you can consider C++ as an alternative.

These language comparisons are mainly qualitative rather than quantitative. After all, I don’t really measure anything, it’s mostly “how I feel”.

Next, I’ll describe the benefits I’ve experienced in Rails. I’ll give a reasonable rating for both extensibility and ease of use.

Finally, of course, there will be the shortcomings that I feel.

But what I feel is not the same as what you feel is a pain point. The size of the application, the amount of organizational resources, and other factors can affect how you feel. So I’m only commenting on Rails based on my own experience and understanding of the evolution of the system.

Ranking

Performance

First, let’s compare performance. As I said earlier, I didn’t do any measurements, I simply did some comparisons based on the nature of the language and my feelings.

C++ > Golang > Node > Python = Rails

Ruby and Python are the slowest, I believe, no doubt, because of GIL, which makes these two languages less efficient. I want to emphasize that Python here means CPython, and in fact, GIL-removed implementations like pypy or IronPython perform very well.

Although Node does not have GIL implementation and is event-driven by nature, Node can still only execute all events through a single process, which is still a gap compared to Golang’s goroutine. C++ has a natural advantage due to its closer implementation to the kernel.

Portability

The portability here is not about cross-platform porting, after all, these languages are cross-platform, and C++ can also be cross-platform as long as it has a corresponding toolchain.

So what is the comparison here? I think we can compete in terms of how easy it is to run an application. For example, Python requires the installation of an interpreter, not to mention the dependency on packages like pip install, and the same applies to Node. But Rails, in addition to Ruby itself, sometimes even Node is part of the dependency, and that’s where it loses out.

Therefore,

C++ > Golang > Node = Python > Rails

In addition to judging by the number of package dependencies, we can also compare the container image sizes created with best practices, and the results are the same.

Ease of coding

The first two comparisons I feel there should be no debate about the results, not much difference with the facts, but the next one is more subjective, the ease of coding.

Python = Rails > Node > Golang > C++

First of all, I am a fan of weak typed languages, so Golang and C++ must be at the bottom of the list. Golang is a bit easier than C++, with fewer keywords.

Although Node is a weak typed language, its event-driven nature makes it a bit difficult for people who are new to it to understand when to async/await and when to synchronize, which takes time to get used to. In addition, Node has a lot of difficulties with object-oriented implementation, and honestly, Node is not easy to get objects right.

As for Python and Rails, they are really easy to code, with a procedural language base and rich syntactic sugar that makes everything possible. If I had to decide between the two, I’d probably vote for Python!

Extensibility (including packages and ecosystems)

Excluding C++, all the other languages have relatively good package management tools, and all have active ecosystems, so it’s a little hard to tell the difference.

But if I have to say so, Python can do a little more than the other languages, so Python is in the front of my list.

Python > Rails = Node = Golang > C++

Benefits of Rails

The above comparison is mostly based on my experience, which may not be the same as yours, but in general Rails is still outstanding.

In particular, as you can see from the descriptions in my first two articles, it can be really fast to build an application with Rails.

Especially since Rails provides excellent ERb templates that make frontend pages easy to develop, and there are many helpers in Rails that integrate well with the frontend to make things even easier.

Furthermore, one of the most amazing features of Rails is the ActiveRecord and resource-based routing paradigms that are developed in accordance with the Active Record Pattern mentioned in Patterns of Enterprise Application Architecture, which shows the author’s ingenuity.

In Rails, you don’t need to care about the database underlying the ActiveRecord. The application is basically written the same way, whether it’s MySQL or MongoDB. More importantly, ActiveRecord can also automatically generate schema for database migration, both in whole and in part, and it can be both up and down, which is really convenient.

Even when we were developing Node, we used Rails to create the schema for the database migration.

Finally, Rails has a lot of built-in features that are necessary for the backend development, such as caching through CacheStore, messaging through ActiveMailer, and job scheduling through ActiveJob. Basically, all the tools for backend development are available, and the underlying infrastructure can be modified with a simple setup, without the need to modify the application.

All of these advantages make Rails the best candidate for a fast development and quick launch.

Shortcomings of Rails

Nevertheless, Rails has some shortcomings.

First, due to the resource-based routing design, all domain knowledge is built around resources. This may not be a problem in a small to medium-sized application, but when the application becomes large, the resource-to-resource constraints increase significantly and resource-based routing is obviously not sufficient.

Let me use the most common e-commerce website as an example.

Suppose there are three resources, order, payment, and transaction history. A typical Rails routing design would generate three routes, all with CRUD capabilities.

/api/orders/{oid}
/api/payments/{pid}
/api/transactions/{tid}

When a user places an order, the operation is performed through /api/orders/{oid}, either to update the status or to get the status. The user then proceeds to make a payment, so a transaction is created and managed through /api/transactions/{tid}. But at the same time, the status of the order will need to be changed to in progress, and a payment will need to be inserted but indicated as unconfirmed.

Based on the above description, we need three API calls.

POST /api/transactions
PUT /api/orders/{oid}
POST /api/payments

Question, how do we ensure that all API calls are correct in such a situation? This is a typical distributed transaction, and I have already written several articles on the difficulties of distributed transactions.

As resource-to-resource connectivity grows with the complexity of the domain, it becomes obvious that Rails’ most classic routing design is overburdened. In addition, all the domain knowledge is fragmented by the resources, which is a common problem in system design, the anemic model.

Moreover, when actually doing a code review, I found that Rails was not as readable as I expected. Even though it’s a resource-based routing design, I often couldn’t find a specific route, especially in a config/routes.rb with thousands of lines. I was always confused by the various combinations of concern, resource and namespace.

In addition to not finding routes, method calls are often not found. Because a class can get the ability of other modules by include, but this include doesn’t require any “declaration” at all. Here the declaration refers to from x import y in Python or import { y } from x in Node, but in Rails there is no need, you can just include y.

So when a method is used in a class, I have to look through include, extend and pretend, and again, when the application is huge, this process really kills me. When there are methods with the same name, it can be several times worse.

One more point is that Rails wraps a lot of implementation behind a layer of objects, making the whole thing feel like a black box. Without a clear understanding of the underlying implementation, it’s easy to code something that seems to work, but actually performs horribly, like N + 1 queries.

If it is a simple object this can save a lot of implementation efforts, such as CacheStore.

I like Rails’ CacheStore, because it provides a lot of common functionality for caching. But when it comes to ActiveRecord, I honestly don’t have confidence that it will work very well. I’d rather write my own database queries, at least I can be sure that the performance of the queries will be good enough.

Conclusion

Although Rails can quickly generate an application skeleton through scaffold, it is very adaptable to fast launch requirements. But I have to say that this advantage can be replaced very easily, especially nowadays when the ecosystems of various languages are so well organized that it is not difficult to do so.

In the case of Node, there are now many MERN generators that can do similar or even better things.

For example,

https://docs.dhiwise.com/knowledgehub/build-node.js-app/Build-app

An e-commerce site can be built at a mouse click and is production ready. So there are few advantages left in Rails, and the design of ActiveRecord, as mentioned in the previous section, is not an advantage but a disadvantage when the application becomes large.

Even so, I would rate ActiveRecord very highly. For those who want to move from procedural languages into the object-oriented world, the design offers plenty of perspective, and once you get used to ActiveRecord, I believe that anyone can build elegant objects.

Besides, I personally like the design of the CacheStore. In addition to the built-in expiration mechanism :expires_in, there is also the elegant :race_condition_ttl to avoid Dog-pile Effect, which is definitely a benefit for people who used to build their own wheels. But I haven’t been in Rails long enough to study what the underlying implementation is, so I’ll leave that for later.

Overall, I feel that Rails is amazing, has a lot of design wisdom in it, and is very classic.

But maybe Rails was originally intended for people who wanted to build applications quickly, and when applications became large, most of the original design became useless and ineffective. This takes us back to the basics of Ruby as a language. For Ruby, the ecosystem is complete and the syntax is flexible enough to build large applications.

But if Rails has to rely on Ruby in the end, why don’t I just use Python? The ecosystem is more complete and the syntax is more flexible.

Anyway, these are my thoughts on Ruby on Rails, and my next goal is to decompose these legacy large Rails applications into microservices. The next article will introduce the classic approach to decompose microservices.

--

--

Chunting Wu

Architect at SHOPLINE. Experienced in system design, backend development, and data engineering.