Read models denormalisations

Andrzej Śliwa
Thoughts not only about Software
2 min readSep 25, 2017

Today we had interesting discussion with Daniel Amireh about how far we can denormalise the read models. Lets say that we have aggregate IPRange which contains all IP addresses no matter if are assigned or not. The responsibility of IPRange aggregate is to track which IP addresses were assigned to which device, which one are taken.

our example aggregate can looks like:

class IPRange
include AggregateRoot
...
def assgin_to(device_id)
apply(IPAssigned.new(device_id: device_id, value: @available.last)
end
...
def apply_ip_assigned_event(event)
@assigned << @available.delete(event.value)
end
end

Originally we come up with solution where we made just simple table of IP addresses:

create_table :ip_addresses do |t|
t.string :state
t.string :device_id
t.string :value
end

Lets say that we have such event emitted from IPRange aggregate:

class IPAddigned < Infra::Event
attributes :device_id, Types::DeviceId
attributes :value, Types::IP
end

We can write such denormaliser handler which will react for IPAssgined event:

module Denormalizers
class IPAssigned
def call(event, repo: Repository)
repo.create_ip_address(value: event.value,
device_id: event.device_id
state: "assigned")
end
end
end

The result of calling such denormaliser will be new row in table with specific state and informations which are important from the read site.

Theoretically we can finish on it, But…

Why not go with denormalisation even further and just create table dedicated just for keeping IP addresses in specific state? This will lets us eliminate additional conditions on read side. Daniel Amireh proposed just table which keeping only things in specific state:

create_table :assigned_ip_addresses do |t|
t.string :device_id
t.string :value
end

Then denormalizer will be also simpler:

module Denormalizers
class IPAssigned
def call(event, repo: Repository)
repo.create_assigned_ip(value: event.value,
device_id: event.device_id)
end
end
end

It’s simple example but showing one important thing, read models connected via denormalisers gives us full flexibility on how we optimise reading of data. We are no logger forced to normalise, we can focus on process modelling with using events. Its because our state is keep in event store, which is the source of true when in same time read models are computed based on events applied on aggregates and published (over queue, pub-sub etc.)

This means that we can also choose databases which better fit for particular use cases, like graph databases or key value stores. Instead of doing expensive queries we can just update counters values in redis (which will play the role of being read model for specific use case) on specific or group of events. So I will nail it again, read models are optimised for read side, to we can denormalise database tables to avoid expensive operations or joins. Important part of using of Read models is to not use them directly over bounded context borders, and try to model aggregates to keep all necessary data to make operations, depending on read models coming always with eventual consistency penalty.

Call for Action

Dear Reader, what is your opinion about it? How you are using your read models? How you are denormalising them? (maybe you don’t, for specific reason) How you keeping them up to date?

--

--

Andrzej Śliwa
Thoughts not only about Software

CTO, Polyglot Developer. I’m coding since when software was stored on 5'25 floppies, getting into philosophical debates