The most important boundary in your app

… and check why 5600+ Rails engineers read also this

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.

Introduction

To illustrate the importance of decoupling, let’s look at the changes in ActionController::Parameters in Rails 4.2 to 5.1.

Rails 4.2

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

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 missing_method hook.

Rails 5.1

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, #map, #each_key, #each_value, etc., were no longer available on ActionController::Parameters instances.

The problem

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.to_hash
params.to_h
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 ActionController::Parameters instances.

In multiples places they were checked against an inheritance from Hash:

# ... 
return results unless hash.kind_of?(Hash)
# ...

They were even passed to the storage layer and serialized with ActiveModel serialize method.


class Attachment < ActiveRecord::Base
  serialize :data, Hash
  # ...
end

When trying to upgrade Rails, existing tests started to fail. We saw a bunch of ActiveRecord::SerializationTypeMismatch errors.

Always decouple

A seemingly simple Rails upgrade turned out to be time-consuming because the domain services were coupled to the internals of ActionController module. 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.

You might also like