Spaghetti Model Part 5: Safely Remove ActiveSupport Concerns

Todd Sedano
Gusto Engineering
Published in
3 min readMar 4, 2024

Note: This is one part of my journey to tame a spaghetti model or god object. Start with Post 1: Unraveling a Spaghetti Model to see how I chose to tackle this problem.

Your spaghetti model has been a dumping ground of methods and associations for years. In our case, it’s the company model. We know we need to add functionality to the system, so we place the new capability on the company because it’s convenient. We may rationalize it with “Besides, the company should know whether it has active medical benefits.”

Overview

To deal with models with way too many methods, Rails 4 introduced ActiveSupport Concerns as a way of partitioning and reusing model code. Methods and associations that worked together could be extracted into a Concern.

While this made the code more manageable in the short term, each spaghetti model still had the same number of methods. Further, these concerns may or may not be good abstractions. Instead, we’d like to move domain logic into domain APIs.

An Example

Here is an example ActiveSupport::Concern, the company model now has the method company.origination_bank , the association company.payer_origination_banks, and a lifecycle callback setup_banking! that is called each time the company is saved.

def company
include GustoBankAccountable
end

module GustoBankAccountable
extend ActiveSupport::Concern
extend T::Sig

included do
has_many(:payer_origination_banks)
after_commit :setup_banking!, on: :create
end

def origination_bank

end

def setup_banking!

end
end

Analysis

  1. List all of the active support concerns and identify which contain domain logic that should be moved to other packs.
| Concern                               | Decision           | # Models | 
|---------------------------------------|--------------------|----------|
| Sluggable | Stay | 1 |
| Bank Accountable | Move to domains | 1 |
| Analytics:Identifyable | Unused so deleted | 2 |
| PaymentsAndFilingsReschedulable | Move to payments | 15 |
| RiskAssessable | Move to risk | 1 |
| BankAccountable | Move to payments | 2 |
| AttrSquishable | Stay | 14 |
| CompanyFormsObserver | Move to forms | 1 |
| CompanyOnboardingFunnelUpdateable | Move to onboarding | 5 |
| ZensaObserver | Stay | 36 |
| AttrBoolean | Stay | 27 |
| DirtyNestedAttributes | Stay | 14 |
| AttrMemoized | Stay | 49 |
| ExternalIdentities::IdentityDeletable | Stay | 4 |

2. Start with active support concerns that are used by 1 model.

Action

Our goal is to move or remove all the methods, associations, and lifecycle callbacks in the ActiveSupport::Concern.

Steps

  1. For each method in the ActiveSupport::Concern, identify where it should live and then follow the techniques in https://medium.com/p/b62631b0f052

2. Move active model lifecycle methods to the model. You can remove the concern now and deal with lifecycle methods later.

# company.rb
has_many :payer_origination_banks, as: :payer, dependent: :destroy

after_commit(on: :create) do
Payments::Banking::PayerOriginationBanks.setup_banking!(payer: self)
end

3. When all that is left is an empty concern, let’s celebrate! 🎉

module GustoBankAccountable
extend ActiveSupport::Concern
extend T::Sig

included do
end
end

It’s time to say goodbye. 👋

Summary

Tackling an ActiveSupport::Concern is the same as handling methods, associations, and lifecycle callbacks.

Originally published at https://sedano.org on September 27, 2023.

--

--