Interfaces for logical migrations
This post explains how you can use interfaces to make data model and database migrations easier.
Imagine you run a B2B company and Account represents how you want to bill each of your clients. Account is both a table in your database and an object in memory that has:
- a
name - a
balanceproperty for how much balance is owed by the account - an
ownerproperty representing the person to email the bills to permissionsrepresenting which users have access to thisAccount
In Ruby1, that might looks like this:
class Account
def name(); end
def owner(); end
def balance(); end
def permissions(); end
end

But over time, requirements change:
- The finance and marketing departments of your biggest client want their bill broken down in two so that they can own separate profit and loss statements.
- They still want to have the same
ownerthat receives one bill but with the subtotal.
You realize, that you want to represent things with:
- Two
Accounts, one for finance, one for marketing where you track the separate balances. - One
Organizationfor the entire company which pays the final bill.
You would like to have both of these objects in your database.

Looking at the new database schema, you might think that you need to run a migration right away. And this would be bad for two reasons:
- Freezing existing code: There is existing code to migrate that is getting enhancements and bug fixes daily. A database migration like the one described usually freezes the codebase.
- Pausing new code: The Security team is trying to redo how permissions work. They agree that permissions is something that conceptually belongs at the
Organizationlevel. But they can't wait untilOrgnizationexists to start their work.
But if you add a level of indirection, nobody needs to wait for the database migration. You can logically migrate the codebase before you the database migration.
You start by introducing an interface for Organizations, called IOrganization. If an object implements IOrganization, it is as-if they were an Organization:
module IOrganization
def owner(); end
def accounts(); end
def permissions(); end
end
Then, you create a temporary in-memory object, AccountAsOrg that makes an Account look like an IOrganization to the rest of the codebase:
class Account
class AccountAsOrg
def initialize(account)
@account = account
end
include IOrganization
def owner()
@account.owner()
end
def accounts()
return [@account]
end
def permissions()
@account.permissions()
end
end
def organization()
return AccountAsOrg.new(self)
end
# rest of Account logic...
def name(); end
def balance(); end
end
All new code can target IOrganization which works for the existing AccountAsOrg and will also work for Orginization when that is ready:

(1) Existing code can migrate in place
For the existing code, you can start to logically migrate it to work with IOrganization wherever you think that is more appropriate:
# Old Billing module:
def send_bill!(account: Account)
owner = account.owner()
balance = account.balance()
email_body = <<~TEXT
Your next bill is:
Total: #{balance}
TEXT
send_email!(owner.email, email_body)
end
# calling the function:
send_bill!(account)
# New Billing module:
def send_bill!(organization: IOrganization)
owner = organization.owner()
accounts = organization.accounts()
subtotals = accounts.map(&:balance).join("\n")
total_balance = accounts.map(&:balance).reduce(:+)
email_body = <<~TEXT
Line items:
#{subtotals}
Total: #{total_balance}
TEXT
send_email!(owner.email, email_body)
end
# calling the function:
send_bill!(account.organization())
(2) New code can directly target the interface
Instead of working with account.permissions(), the Security team can directly target organization.permissions():
module Security
# this module never references Account
def check_permissions_integrity(org: IOrganization)
permissions = org.permissions()
# ...
end
end
# at the very edge of the module:
check_permissions_integrity(account.organization())
Logical migration first, database migration later
Later, you can do the actual database migration and create a table with all the Organizations:
- Each
Accountpoints to oneOrganizationand vice-versa. - Each
Organizationpoints to one or multipleAccounts.
The new class Organization implements IOrganization, so you can just start passing it wherever IOrganization was expected:
class Organization
include IOrganiation
def owner(); end
def accounts()
# lookup accounts in the database and return them
end
end
organization = Organization.new(...)
# In the Security codebase
check_permissions_integrity(organization)
# In the Billing module
send_bill!(organization)
and account.organization() returns Organization instead of the temporary AccountAsOrganiztion:
class Account
def organization()
# looks up the Organization and returns that
Organization.new(...)
end
...
end
Notice that the existing call-sites using account.organization() don't need to migrate:
# Billing code works:
send_bill!(account.organization())
# Security's code works:
check_permissions_integrity(account.organization())
Footnotes
- In Java, C#, TypeScript we would use
interface, in Clojureprotocol, in Haskelltypeclass↩