I’m switching from Laravel to Rails

reshadman

Reza Shadman

Posted on June 20, 2024

I’m switching from Laravel to Rails

I have been using Laravel since version 4 in 2013. Over the years, Laravel has evolved significantly. I initially chose Laravel over Rails due to its favorable position in our local job market. In 2015, I started building our own business using Laravel. Today, that business is the largest job board in Iran, serving over 3 million job seekers and 100,000 employers. Laravel performed well for us, until it didn't.

Over the years of maintaining this application, I have come to some conclusions, both in terms of code and team dynamics. Our entire product/tech team has never exceeded six people, including designers and product managers. During the COVID-19 pandemic I managed everything solo Product/Tech wise.
I have also been part of projects using Spring, Symfony, and Django. We were among the first adopters of Vue.js back in 2015, starting with Vue 0.12.

I told you the story to picture the pains I've been encountering during maintaining a Laravel application over 9 years.

Sticking to framework defaults
Over the years, I have realized that adhering to framework defaults at least 90% of the time is the best way to ensure easy upgrades, address security concerns, adopt new technologies, and hire new developers. Even though there are architectural trade-offs, the benefits outweigh the drawbacks.

Laravel changes opinions and defaults
Laravel tends to change its opinions with every major version or introduce new ones. Most of the time, these new opinions permeate every part of your application. In contrast, Rails sticks to its architectural roots. The "Rails way" remains similar to how it was 10 years ago, allowing you to join a project that respects this method and perform easy upgrades, onboard new developers, or add features seamlessly. Laravel, however, is not like that.

Over the past 10 years, Laravel has promoted various combinations, such as (Vue + Laravel), (Laravel + Inertia), Livewire with class components, and Laravel Folio + Volt. This is just on the framework side. The community, including the framework authors and the crew (like Laracasts), has promoted multiple nonsensical architectural ways of writing software. Many of these make no sense, such as repositories with active record models, repositories as service objects, service objects with models, service objects with models and repositories, and service objects with in/out DTOs.

I found my way early on, but this inconsistency makes it hard to find new developers and maintain a consistent codebase. Every community has its challenges, just like the ones I mentioned, but I have never seen DHH, for example, seem confused about how to write Rails software. Look at the source code for first-party Laravel packages like Breeze Scaffoldings, Laravel Fortify, Laravel Telescope, Laravel Sanctum, Laravel Spark, Laravel Pulse, and Laravel Horizon. Each of them has different choices on how to write software, which is precisely the problem I’m facing. Even the framework owner seems inconsistent on how to write business software or near-end user components with Laravel. There are too many choices, and some are abandoned while others are equivalent to each other.

Laravel is somehow merchant of complexity
People are always eager for new content, new tools and new approaches to writing software. Developer tooling authors often build their business plans around this demand, which isn't necessarily bad, but it does have side effects. This is the problem when a dedicated company including a full-time team works on a set of open-source products: the framework is designed for revenue after some years. Basecamp does a ton of marketing with Rails, but I respect how they keep their commercial software separate from their open-source extractions.

Part of these new opinions on how to write software stems from trying to attract everyone and every method. In recent years, Laravel has turned into a sort of food court. Need MVC? We've got that. Think Phoenix LiveView is cool? We create Livewire as a first-party package and promote it everywhere in the docs. Is the React handler+view in one file hyped? We create Laravel Volt + Folio. People love React and Vue but find writing Next and Nuxt hard? We create Inertia. We have responses for every question and consideration.

Yet, from a developer's point of view, you have to keep up with every new cognitive load you're faced with. Nearly all of them are just noise, with only a few, like Inertia, offering something valuable. Sometimes, the old ways are completely abandoned. Although not entirely without backward compatibility, they are neglected and receive no care. Laravel has somehow become a merchant of complexity, constantly introducing new paradigms that add to the cognitive load for developers. Then, they sell courses, books, commercial packages, and companion software around these new paradigms. I'm really okay with this part, but it significantly affects community packages and standards, developer skills, and codebases, this is the part I'm not OK with.

Enough philosophical bullshit—let's talk about code.

ActiveRecord is years ahead of Eloquent
Eloquent has mimicked ActiveRecord but hasn't brought over the most important parts. ORM is a key component of a battery-included framework, and Eloquent has received nearly zero upgrades since 2015.

Part of this is due to Ruby's influence, but that part is manageable. Eloquent lacks several crucial features, such as self-validating models, commit callbacks, and models acting like aggregate roots. This isn't just about putting validation rules in Laravel form requests, controllers, or models; it's about validations being an important part of the model's existence. They fundamentally change how you define behavior, states and solve problems. In Rails, it's much harder to put a model in the wrong state because of self-validating models, models acting as aggregate roots, and model callbacks being first-class citizens.

With Eloquent, you have to wrap nearly every multi-model interaction in an explicit transaction block, and I've paid the price (even financial price) for missing it, which is easily available in other ORM libraries. Eloquent behaves like a Row Gateway during writes, making it easy to corrupt data states in different parts of your app.

The syntax in Ruby for defining validation rules on a model, accessors, or callbacks is far better than the options available in PHP. I love Active Record pattern because of its progressive behavior during development, but I've reached a point where I define rules, getters, setters, casts, fillable, and guarded attributes independently, instead of defining them for a single field in a data mapper ORM with poor syntax that combines object behavior definition with metadata. How can a battery-included framework that offers multiple ways of building front-end! not offer optimistic locks or commit callbacks for models? I mean callbacks, not event listeners, which introduce the highest level of possible indirection when scanning code—another problem in itself.

Service providers are PHP bullshit
Rails has the advantage of not requiring dependency injection (DI) or inversion of control (IoC), so you don't struggle with IoC on simple tasks like saving an input field to the database. You just see business code in your implementation. In contrast, Laravel uses facades and extensions to address this, but they feel like hacks. As a result, you're left with pages of configuration files and limitations on extending functionality.

Blade is better than ERB
I like ViewComponent in Rails, but even ERB with ViewComponent doesn't match the capabilities of Blade, especially when it comes to components. The XML-like syntax for writing templates is really nice.

About Routes, FormRequests, Policies and Middelwares
When you write a CRUD operation in Laravel, you might end up defining nearly five classes: two Form Request objects, one controller, a policy class, and a simple model, each existing in a separate folder within the application. One of my experiences in programming is the importance of keeping related things together. Rails still has this problem, but you don’t necessarily have to define five classes for a single feature. Using Livewire? You'll need even more classes. Despite this level of abstraction, it's not necessarily enough; there are tons of business-logic-filled middlewares in every Laravel app.

Rails' controller lifecycle callbacks, together with concerns, are a blessing. You still write simple Ruby code without defining a dozen layers just to save a form to the database and introduce some side effects. Route model bindings seem very attractive at first sight, but in a real Laravel project, you either see tons of query logic in explicit route model binding definitions leaking through service providers, middlewares, and route files, or you find yourself performing repetitive find queries for the same fetch logic in hundreds of controllers, passing them to view files each time. Alternatively, you might invent a new class/service/action for these jobs, which every implementation differs from every other Laravel app you've seen.

When learning Laravel, the documentation heavily promotes route model binding, but it seldom mentions that they are not nested through current user and that you MUST define policies for controlling access to them. Just search for sites using Livewire JS file on the internet, create accounts on those sites, and you will likely find many that allow you to access other users' resources by changing parameters in URL.
In Laravel, the routes definition file sometimes contains more logic than your controllers and models combined. Read the documentation for gateways and policies on the Laravel site; they offer dozens of methods for doing the same thing—the scroll is longer than the HTTP server implementation file in Go.

Form Requests advertise nothing about abstraction. Open a standard Laravel project, and you'll see repeated code for rules and authorization everywhere. I've developed the same app for nine years, and believe me, this simple thing annoyed me the most. The annoyance was doubled when they introduced Form Requests. Rails, by default, uses request-lasting query caching for fetching ActiveRecord queries. Open a Laravel app, and you'll see repeating queries in middlewares to control payment walls, bans, etc. There’s still no clear way of sharing data from middleware to controllers. You have dozens of options: singleton classes, request merging, request extending, etc., but no one uses them effectively. At some point, you don't even know what middlewares are bound to your route, and you get surprised.

Laravel controller methods nearly just function as input-output handlers, which is okay, but combining them with dozens of layers and concepts to perform a simple before_action callback directly in the controller or the parent controller is excessive.

Rails has much less noise by not having these features. Rails carefully advertises the way it does things and carefully chooses what not to include to prevent becoming feature-bloated. This is a feature not a bug.

Rails Documentation
At first sight, Laravel seems more documented than Rails, which is true when considering a beginner's perspective. However, the feature of Rails writing the real documentation alongside the code itself is really, really nice. You figure out by Ctrl+B and feels really productive.

Hotwire is better than Livewire
I really believe that simple MVC with strict resource controllers is the best way to decompose an app with hundreds of screens. However, Livewire somehow prevents this. You write your logic alongside some JavaScript-related code, and changing the UI significantly impacts your backend implementation since they live together in the same component. This leads to a lot of dead code in Livewire for long-term maintenance projects, and edge cases are really hard to fix. Additionally, the Livewire way of writing software is deeply specific in its own environment. For instance, I can discuss about models, controllers, before_action, or middlewares—concepts that exist in nearly every framework or library. However, developing software in Livewire, involves a very specific and unique implementation that doesn't translate well to other approaches.

I've also found that 90% of JSON API exposures are just response format changes, which is strongly utilized in the Rails community and is much harder and nearly impossible when using Livewire. With Livewire, you end up writing multiple controllers that do the same thing. Sticking to this design may seem hard at first, but it really keeps the application maintainable and understandable in the long run.

Additionally, I really wish development teams would recognize the value in the Shape Up method. One of its key components during cycles is progressive development. If you want to implement a feature, you can start by writing dead simple HTML and then implement the functionality, wiring them together and adding interactivity later. I think Rails encourages this behavior by the way Hotwire works, and it is really wise.

I also believe that creating TRULY VERY high-fidelity UIs with Hotwire is not possible or practical, but I have accepted the trade-off. Most of the time, I struggle to create those UIs even in environments like Vue. Overall, I really like the progressive approach of Hotwire and the low footprint it introduces, but I think Alpine.js is a better alternative to Stimulus.

Rails developers seem to be more skilled in Rails' front-end offerings because it recommends a single approach through Hotwire. In contrast, Laravel offers multiple stacks, making it harder to find a full-stack developer. At least in our local community.

Notifications and Mailer
Laravel allows defining notification classes that can have multiple destinations: email, push notifications, database, webhooks, etc. Rails has mailers, but they are not designed out of the box with the same semantics as Laravel's notification system. I really wish Rails would make this section more unified by introducing web push notifications in Rails 8. The same lack of important features applies to background jobs and writing console commands.

First party packages and Ecosystem
At first sight, it seems Laravel has tons of first-party packages that do a lot of things, but I really think they create much more noise. The Rails community seems more focused on promoting building actual applications, while the Laravel community seems more focused on developer tooling (not developer experience), keeping developers busy figuring them out. That's why you see more end-to-end apps written in Rails on GitHub. Many communities have had their hypes, but relatively few have produced projects like GitLab, Postal, Mastodon, Redmine, and so on compared to other types of open-source projects.

That's the real power of an ecosystem. You have the option to see what real-world code looks like by exploring some of the best end-user-facing applications. I was around the Laravel community for more than a decade and never found a project that improved my knowledge as much as something like Postal or GitLab did. The Laravel community is warm and welcoming, but they are heavily focused on developer tooling.

I don't know if I've successfully transferred my point, but Rails and Django might be the only full-stack frameworks that give me the feeling that you can be cool and show off by building or teaching how to create end-to-end applications, not just by creating libraries.

The only true masterpiece in the Laravel ecosystem, for me, is Forge. It is truly excellent and frees your mind from many concerns. As for libraries, they already exist in the Rails community or are some fancy additions to my workflow. The real pains I experience are those mentioned above.

Conclusion
In the end, I'm really thankful to Laravel. It allowed me to make real money as a young developer who was just eager to build a good life through the internet. However, I have chosen to continue my path with Rails. I feel happier and more productive when I write in Rails compared to Laravel. While I can customize Laravel to my needs to a certain extent, as I mentioned earlier, there is great value in sticking to defaults. Alternatively, if you don't want to leverage the offerings of a battery-included framework or they require extensive customizations, you might consider using very verbose and explicit languages like Go. However, these customizations can make tasks like finding a new developer or performing upgrades more challenging.

This is in no way a recommendation for the job market. Job market dynamics vary from place to place and are not usually influenced by personal preferences or personal considerations like the ones I mentioned above or being a one person framework. Typically, the most hyped technology is the one that commands the highest salaries. From this perspective, TypeScript/JavaScript is currently more hyped than anything else on the planet.

💖 💪 🙅 🚩
reshadman
Reza Shadman

Posted on June 20, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related