The final trick when moving from Rails CRUD to Event Sourcing

… and check why 5600+ Rails engineers read also this

The final trick when moving from Rails CRUD to Event Sourcing

From CRUD to EventSourcing

Did you ever wonder how to actually switch the model from the CRUD one to EventSourcing? There’s one thing you should consider. The initial event that has to be published for existing data. Also called the migration event.

At some point, one will reach a moment when they’re able to switch from the old model to the new one. Well crafted, event sourced aggregate. With 100% mutant coverage.

But then the questions pop up… How do you migrate data from legacy model to the new aggregate? How should you seed the aggregate with the migration event?

Before you start, you have a decision to make

Basically you have at least two options when it comes to the migration event.

The first option is to start with the event that “starts” the life-cycle of your aggregate. If your aggregate is a BankAccount that is brought to life by open method, which produces BankAccountOpened event, you would migrate your legacy model by calling the method to produce this event. You could do that by script, example below 😉.

The second option is a little bit different. Instead of starting with regular event that starts the lifecycle of the aggregate, you can introduce a new one that will be used only for migration. For the initial opening balance. In case of BankAccount the migration event could be named LegacyBankAccountImported.

Additional, unnecessary work you might say.

This approach has one huge advance. In the future if you have to analyze the stream, you’ll be able to distinct aggregates that were migrated from aggregates that were created after the migration. From the debugging perspective this is very useful piece of information. I usually prefer that way.

Let’s deal with the initial event

Once you know which approach you want to follow, you can use a script similar to the one below:

def stream_name(aggregate_id)

legacy_bank_accounts = BankAccount.unscoped

repository =

legacy_bank_accounts.each do |legacy_bank_account|
  aggregate_id = legacy_bank_account.uniq_id
  ApplicationRecord.with_advisory_lock(legacy_bank_account.uniq_id) do
    repository.with_aggregate(, stream_name(aggregate_id)) do |bank_account|
        bank_account.import_deleted(legacy_bank_account.balance, legacy_bank_account.balance_date)

As you can see, the script loads the data with old model. Then it iterates through that data and calls one of the import methods (depending on the legacy model state), producing the migration event for the aggregate. The event in this case is either LegacyBankAccountImported or ClosedLegacyBankAccountImported

And that’s it. You’re ready to switch to the new model now 🙌

PS. After the initial migration I recommend to remove the import method. The reason is that you don’t want anyone to use the second time after the first usage. It would be a a little bit missleading durning some debugs.

You might also like