One more step to DDD in a legacy rails app

— Published originally on blog.arkency.com

One more step to DDD in a legacy rails app

Recently I picked up a ticket from support team of one of our clients. Few months ago VAT rates have changed in Norway - 5% became 10% and 12% became 15%. It has some implications to platform users — event organizers, since they can choose which VAT rate applies to products which they offer to the ticket buyers. You'll learn why I haven't just updated db column.

Current state of the app

This app is a great example of a legacy software. It's successful, earns a lot of money, but have some areas of code which haven't been cleaned yet. There's a concept of an organization in codebase, which represents the given country market. The organization has an attribute called available_vat_rates which is simply a serialized attribute, keeping VatRate value objects. I won't focus on this object here, since its implementation is not a point of this post. It works in a really simple manner:

vat_rate = VatRate.new(15)
# => #<VatRate:0x007fdb8ed50db0 @value=15.0, @code="15">

vat_rate.code
# => "15"

vat_rate.to_d
=> #<BigDecimal:7fdb8ed716c8,'0.15E2',9(27)>

VatRate objects are Comparable so you can easily sort them; pretty neat solution.

Event organizer, who creates eg. a ticket, can choose a valid VAT rate applying to his product. Then, after purchase is made, ticket buyer receives the e-mail with a receipt. This has also side-effects in the financial reporting, obviously.

So what's the problem?

I could simply write a migration and add new VAT rates, remove old ones and update events' products which use old rates. However, no domain knowledge would be handed down about when change was made and what kind of change happened. You simply can't get that information from updated_at column in your database. We have nice domain facts "coverage" around event concept in the application, so we're well informed here. We don't have such knowledge in regard to the Organization.

Start with a plan

I simply started with making a plan of this upgrade.

  1. I've checked if the change made to available_vat_rates will be represented properly in the financial reports.
  2. I've checked how many products were having old VAT rates set.
  3. I've introduced new domain events called Organization::VatRatedAdded and Organization::VatRateRemoved which are published to the Organization$organization_id stream.
  4. I've run a migration, which was adding new VAT rates (10% & 15%) and publishing sufficient domain facts — let's call it step 1.
  5. I've performed an upgrade of the VAT rates on the products which required it - step 2.
  6. I've run a migration, which has removed old VAT rates (5% & 12%) and published domain facts - step 3.

Step 1 - adding new VAT rates

require 'event_store'

class AddNewVatRatesToNoOrgazation < ActiveRecord::Migration
  #… minimum viable implementations of used classes

  def up
    event_store   = Rails.application.config.event_store

    organization  = Organization.find_by(domain: 'me.no')
    originator_id = User.find_by(email: 'me@me.no').id

    organization.available_vat_rates = [
        VatRate.new('NoVat'),
        VatRate.new(5),  # deprecated one
        VatRate.new(10), # new one
        VatRate.new(12), # deprecated one
        VatRate.new(15), # new one
        VatRate.new(25),
    ]

    if organization.save
      event_store.publish(Organization::VatRateAdded.new(
        organization: organization.id,
        vat_rate_code: 10,
        originator_id: originator_id
      )
      event_store.publish(Organization::VatRateAdded.new(
        organization: organization.id,
        vat_rate_code: 15,
        originator_id: originator_id
      )
    end
  end
end

Two things worth notice happen here. Event data contain originator_id, I simply passed there my user_id. Just to leave other team members information about person who performed the change in the event store — audit log purpose. The second thing is that I leave old VAT rates still available. Just in case if any event organizer performing changes on his products, to prevent errors and partially migrated state.

Step 2 - migrating affected product data

The amount of products which required change of the VAT rates was so small that I simply used web interface to update them. Normally I would just go with baking EventService with UpdateTicketTypeCommand containing all the necessary data.

Step 3 - remove deprecated VAT rates

require 'event_store'

class RemoveOldVatRatesFromNoOrgazation < ActiveRecord::Migration
  #… minimum viable implementations of used classes

  def up
    event_store   = Rails.application.config.event_store

    organization  = Organization.find_by(domain: 'me.no')
    originator_id = User.find_by(email: 'me@me.no').id

    organization.available_vat_rates = [
        VatRate.new('NoVat'),
        VatRate.new(10),
        VatRate.new(15),
        VatRate.new(25),
    ]

    if organization.save
      event_store.publish(Organization::VatRateRemoved.new(
        organization: organization.id,
        vat_rate_code: 5,
        originator_id: originator_id
      )
      event_store.publish(Organization::VatRateRemoved.new(
        organization: organization.id,
        vat_rate_code: 12,
        originator_id: originator_id
      )
    end
  end
end

Summary

All the products on the platform have proper VAT rates set, organization has proper list of available VAT rates. And last, but not least, we know what and when exactly happened, we have better domain understanding, we started publishing events for another bounded context of our app. If you're still not convinced to publishing domain events, please read Andrzej post on that topic or even better, by watching his keynote From Rails legacy to DDD performed on wroc_love.rb.

Tags: · · ·

Avatar of Author

Szymon Fiedler

I solve problems. This is the place where I share my thoughts on Software Engineering.