The most important boundary in your app
Recently, we continued working on the update I referred to in my previous post. When planning an update from Rails 4.2 to Rails 5.0 and then to Rails 5.1, I realized again how crucial it is to avoid coupling your application to the framework’s internal details.
To illustrate the importance of decoupling, let’s look at the changes in
ActionController::Parameters in Rails 4.2 to 5.1.
In Rails 4.2,
ActionController::Parameters were a subclass of Ruby’s core
Hash class, making it easy to pass around and use like any other hash.
Rails 5.0 introduced a significant change:
ActionController::Parameters were no longer a subclass of Hash but a separate class entirely.
This change was implemented to improve security and prevent mass assignment vulnerabilities.
However, all the methods available on hashes were still available on
ActionController::Parameters through the
In Rails 5.1, the
missing_method hook was removed.
It further emphasized the separation between ActionController::Parameters and regular Ruby hashes so that methods
#each_value, etc., were no longer available on
As you can see, the difference in public API differs significantly between the following Rails versions.
|Rails 4.2||Rails 5.0||Rails 5.1|
|only permitted params?||has indifferent access?||only permitted params?||has indifferent access?||only permitted params?||has indifferent access?|
|params.(some native hash method)||❌||✅||❌||✅||Missing method error|
The app we were working on has its service layer called from the controllers. The service layer is responsible for handling some business logic. Its outcome is often persisted in the database.
Unfortunately, the service objects were initialized with
In multiples places they were checked against an inheritance from
# ... return results unless hash.kind_of?(Hash) # ...
They were even passed to the storage layer and serialized with ActiveModel
class Attachment < ActiveRecord::Base serialize :data, Hash # ... end
When trying to upgrade Rails, existing tests started to fail. We saw a bunch of
A seemingly simple Rails upgrade turned out to be time-consuming because the domain services were coupled to the internals of
To avoid such problems, it is crucial to maintain a clear boundary between domain logic and framework internals.
Be aware of passing around framework objects to your domain layer. Use standard Ruby types or your own value objects instead.
Decoupling has several advantages:
- Flexibility: Decoupling allows developers to switch frameworks, libraries, or even languages without having to rewrite the entire application.
- Testability: When domain logic is decoupled from framework internals, it is easier to test the core functionality without complex setups or dependencies.
- Maintainability: Decoupled applications are easier to maintain because they have a clearer separation of concerns, making it easier to identify and fix issues.
Similar examples will be addressed in the upcoming Rails Business Logic Masterclass course. Subscribe to the newsletter below so you don’t miss any updates.