Immutable-ish
When modelling certain parts of an application backed by a relational database you are better of thinking of the data as immutable-ish; here follows 2 examples of what I mean by this.
Example 1: User access
We have the following models:
class Account < ApplicationRecord
end
class User < ApplicationRecord
end
class AccountMembership < ApplicationRecord
belongs_to :account
belongs_to :user
scope :active, -> { where(revoked_at: nil) }
end
To give a user access to an account you would do
AccountMembership.create!(account: account, user: user)
To revoke access for a user to an account you would do
AccountMembership
.active
.where(account: account, user: user)
.update_all(revoked_at: Time.zone.now)
When revoking access you are actually mutating the active AccountMembership
records, hence why I call it immutable-ish 😎
The advantage of this data model is that the records themselves will contain timestamps (created_at
: when was access given, revoked_at
: when was access revoked) of what happened and when. This makes it easier to debug the timeline of events without resorting to also keeping track of actual events.
And lastly, we avoid dealing with duplicate records because we would check account access like this
AccountMembership
.active
.where(account: account, user: user)
.exists?
Example 2: Plan subscription
For SaaS products you will usually need to associate accounts with pricing plans. This could be modelled like this:
class Account < ApplicationRecord
belongs_to :current_subscription, optional: true, class_name: "PlanSubscription"
has_many :subscriptions, class_name: "PlanSubscription"
end
class PlanSubscription < ApplicationRecord
belongs_to :account
end
When the account is initially subscribed to a plan we’ll do
ApplicationRecord.transaction do
new_subscription = account.subscriptions.create!(plan_identifier: "small")
account.update!(current_subscription: new_subscription)
end
And if the account upgrades or downgrades we’ll do the exact same thing:
ApplicationRecord.transaction do
new_subscription = account.subscriptions.create!(plan_identifier: "large")
account.update!(current_subscription: new_subscription)
end
Cancelling the subscription will be equally simple:
account.update!(current_subscription: nil)
With this data model we’ll know exactly when an account subscribed to a certain plan. This is especially important if we also want to keep track of invoices:
class Invoice < ApplicationRecord
belongs_to :account
belongs_to :plan_subscription
end
This way every Invoice
record will be associated with the PlanSubscription
which was the current_subscription
the account had at the time the invoice was generated.
With this example we are not mutating existing PlanSubscription
records but we’re updating Account#current_subscription
.
Summary
A database like Datamic uses the idea of immutability in its core architecture. Relational databases can mimick this behavior but I would recommend not using this approach for entities such as User
because you lose the ability to reference data if you keep on creating new records.
A use case I did not mention here is that not mutating data will also allow us to easily revert back for instance if a user accidentally deletes something. Basecamp 3 does this with a concept they call recordings; you can see DHH mention it in this video.