Correlation id and causation id in evented systems

… and check why 5600+ Rails engineers read also this

Correlation id and causation id in evented systems

Debugging can be one of the challenges when building asynchronous, evented systems. Why did this happen, what caused all of that?. But there are patterns which might make your life easier. We just need to keep track of what is happening as a result of what.

For that, you can use 2 metadata attributes associated with events you are going to publish.

Let’s hear what Greg Young says about correlation_id and causation_id:

Let’s say every message has 3 ids. 1 is its id. Another is correlation the last it causation. If you are responding to a message, you copy its correlation id as your correlation id, its message id is your causation id. This allows you to see an entire conversation (correlation id) or to see what causes what (causation id).

Now, the message that you are responding to can be either a command or an event which triggered some event handlers and probably caused even more events.

In Rails/Ruby Event Store this is also possible. Recently we’ve released version 0.29.0 which adds #with_metadata method that makes it even easier. BTW, that was our 52nd release.

class MyEventHandler
  def call(event)
    event_store.with_metadata(
      correlation_id: event.metadata[:correlation_id] || 
                      event.event_id,
      causation_id:   event.event_id
    ) do
      # do something which triggers another event(s)
      event_store.publish_event(MyEvent.new(data: {foo: 'bar'}))   
    end
  end

  private

  def event_store
    Rails.configuration.event_store
  end
end

of course, if you don’t publish many events, it might be easier to apply it manually, once.

class MyEventHandler
  def call(event)
    # do something which triggers another event
    event_store.publish_event(MyEvent.new(
      data: {foo: 'bar'},
      metadata: {
        correlation_id: event.metadata[:correlation_id] ||
                        event.event_id,
        causation_id:   event.event_id
      }
    ))   
  end
end

Now, keeping that correlation and causation IDs in events’ metadata is one thing. That’s beneficial and if you want to check why event X happened you can just easily do it, but it’s not where the story ends.

Imagine that you have a global handler that registered which reacts to every event occurring in your system and building two projections by linking the events to certain streams:

class BuildCorrelationCausationStreams
  def call(event)
    if causation_id = event.metadata[:causation_id]
      event_store.link_event(
        event.event_id,
        stream_name: "causation-#{causation_id}"
      )
    end
    if correlation_id = event.metadata[:correlation_id]
      event_store.link_event(
        event.event_id, 
        stream_name: "correlation-#{correlation_id}"
      )
    end
  end
end

What would that give you?

Given an event with id 54b2 you can now check:

  • in stream causation-54b2 all the events which were triggered directly as a result of it
  • and in stream correlation-54b2 all the events which were triggered directly or indirectly as a result of it (if that message started the whole conversation)

That makes it possible to verify what happened because of X?

I hope that in the future we can automate all of it when some of the upcoming Ruby Event Store features are finished:

Would you like to continue learning more?

If you enjoyed that story, subscribe to our newsletter. We share our every day struggles and solutions for building maintainable Rails apps which don’t surprise you.

You might enjoy reading:

You might also like