by Miguel Angel

How to extract a field from a Rails model using the Mikado Method


In our environment, there are many times when a new field is added to a Rails model; after some time, that field feels that may not belong to the model any more. Let’s look at a simple case.

Originally we created a model in the system- a Payment like this:

create_table "payments" do |t|
  t.string "reference"
  t.integer "amount_to"
  t.integer "currency_to"
  t.integer "amount_from"
  t.integer "currency_from"
  t.integer "exchange_rate"
end

After some time, the business guys find that the company needs to store information about a debtor, specifically her first and last name. With a small Payment model and two simple fields it is very easy to add those fields to the model and move along. There are multiple benefits; the Payment model is already used where it is needed and propagating a couple of fields comes at no cost. In addition, we still don’t know what will happen in the future with the new requirement. Chances are that the fields end up unused. Let’s do it!

create_table "payments" do |t|
  # [...]

  t.string "debtor_first_name"
  t.string "debtor_last_name"

  # [...]
end

A few months later, we check again that part of the code… surprise! The debtor has become an important feature of the program somehow. Debtor has email and we send communications that must be tracked, the debtor sometimes calls customer support and she needs to query his payments to give proper answers, and a few other things that has been built without realizing that it has become an entity for itself within the business.

We’ve experienced this situation and it is becoming almost a pattern. Here is how we deal with it, for better or worse. We checked out the book The Mikado Method a few months ago and we found the technique helpful in these kinds of situations that require performing non trivial changes. Briefly, the Mikado Method consists of a few steps that look like common sense written as a checklist. First, write the main goal. For us the goal is to extract debtor as an independent entity. Next step is to gather knowledge implementing the goal naively, without analyzing the consequences too much. And the final step is to pull out a dependency graph from the conclusions. We call this naive implementation or experiment a spike. The graph drawing includes all the requisites needed to be implemented in order to achieve the final goal. For simplicity’s sake all the requisites are grouped in a few steps that allow for merging and shipping incremental changes.

1. Write the new model and duplicate information in both models

The purpose of this step is to start accumulating records in the new table without the danger of impacting the current behaviour. This step may need to be extended with extra ones in case data inconsistencies are found due to hidden logic, which makes the data diverge at some point after creating the records.

create_table "debtors" do |t|
  t.string "first_name"
  t.string "last_name"
  t.string "email"
  t.string "country"
  t.string "phone"
  # [...]
end
class Payment < ActiveRecord::Base
  extend ActiveSupport::Concern

  included do
    has_one :debtor
    before_save :build_debtor_entity
  end

  def debtor
    @debtor_entity ||= Debtor.find_by_payment_id(id)
  end

  def build_debtor_entity
    debtor = Debtor.find_by_payment_id(id)
    unless debtor.present?
      debtor = build_debtor(first_name: debtor_first_name, last_name: debtor_last_name)
    end
  end
end

2. Read the new fields together with the old fields and check inconsistencies

In this step, all the places where the original fields are used need to be located and changed. The change includes replicating the behavior using the new fields and the old ones.

Let’s take an instance where a user action triggers an email to the debtor, and inside the template body there is something like:

class WelcomeEmail
  # [...]
  def body
    "<p>Dear {payment.debtor_first_name},</p>"
    # [...]
  end
end

The debtor_first_name could be rewritten like this:

class Payment < ActiveRecord::Base
# [...]

  def debtor_first_name
    new_debtor = debtor.first_name
    if(new_debtor != super)
      logger.info("Payment #{id} has a mismatched debtor")
    end
    super
  end

# [...]
end

The overall performance may suffer during a few days but it will provide the confidence that the new data is consistent.

3. Actually use the new structure alone when we feel we are confident enough

As soon as we are sure both systems work the same it is time to deprecate the old code. This step involves looking for all the uses of payment.debtor_first_name and making sure it calls the new structure. Although this could be implemented in different ways, just make sure you are consistent with the interpretation of a DDD Aggregate.

4. Remove old code

And for the last phase, when we are sure we won’t have to apply any hotfix, remove the remains of the code and database fields as if they never occurred.

Software systems start small and over time, as the business evolves, new features try to get space for themselves even if the changes are not planned in a backlog. It is important to raise your eyes and check from time to time those hidden features and move them to their own ecosystem when they can grow healthily.

Miguel Angel Fernández. Developer at Flywire.