Avoid coupling between Bounded Contexts using Weak Schema

… and check why 5600+ Rails engineers read also this

Avoid coupling between Bounded Contexts using Weak Schema

The Rails Event Store comes with a bounded_context gem (and a generator) that simplifies making your application modular. Using the command:

rails generate bounded_context:bounded_context YOUR-BOUNDED-CONTEXT-NAME

you can quickly generate folder structure, add load path and start working on implementing your business logic without friction.

You could see how we’ve used this gem in our sample application showing how to use CQRS and Event Sourcing to implement a sample business process.

The idea of bounded context is to have separate modules, with its own ubiquitous language and with its concepts (you know, a Customer in ordering context could be a DeliveryAddress in the shipping context, and a Customer in invoicing context may be a different concept than the one on ordering context).

But we still need to communicate the business events between contexts. Because no context is an information silo. We build systems. We build things that cooperate. That system cooperation is what makes the difference. This is where business processes are defined. Probably a lot of companies have similar ordering, invoicing & shipping contexts build in their system. Also, most of them are quite similar. At least the non-core ones. Is there any e-commerce company that builds its invoicing system? Yeah, I know Amazon might have one, but do you think it is a reasonable thing to invest in building custom invoicing system by small e-commerce shop? Or it is better to buy access to an existing solution and invest in integrating it into your business process?

Coupling here

And that’s the place where the coupling is introduced. At least in our simple (sometimes too simple) sample application. So where the coupling is?

Event sourced aggregates defined in modules (bounded contexts) are using module defined domain events for both - storing state changes (event sourcing) and for communicating business events between system components (via Rails Event Store pub/sub). The default configuration of RailsEventStore::Client uses a mapper with YAML serializer. Also the RailsEventStore::Event uses class name as event type. And here is the problem.

Why?

Why there is a problem?

This is coupling we have on several levels:

  • coupling of domain events persistence with publishing them to other components, especially to other bounded contexts,
  • coupling of event type to event’s implementation (by using a class name),
  • coupling between bounded contexts as with this implementation all of them must “know” the same class (i.e. shipping BC needs to be able to use ordering domain events).

There are tradeoffs, but we have deliberately made those choices because of several reasons. The main of them was:

  • for the sake of simplicity when you start with Rails Event Store,
  • backward compatibility… because some use that setup in your production projects,

How to decouple

1st: decouple persistence from communicating business events.

You must not store as an internal state change of aggregate and publish outside of the bounded context the same message (event). You could use the mailbox pattern, known from Actor Model to handle incoming messages and a outbox pattern to communicate important business facts that have happened in the bounded context (module). The events used to store aggregate state changes are now only internal implementation of this module and must not be exposed outside of it. Keep them private in the scope of the module. This also means you could no longer use a class name as an event type.

This sample code is a definition of “business” events in separate modules:

module Ordering
  class OrderCompleted < RailsEventStore::Event
    def event_type
      'ordered'
    end
  end
end

module Shipping
  class DeliveryScheduled < RailsEventStore::Event
    def event_type
      'ordered'
    end
  end
end

Overriding the event_type method will allow to identify the event and match it to different event’s classes in both modules. To do it you need to define event class remapping in each module’s Rails Event Store configuration:

module Ordering
  def event_store
    mapper = RubyEventStore::Mappers::Default.new(
      events_class_remapping: {
        'ordered' => 'Ordering::OrderCompleted',
      }
    )
    RailsEventStore::Client.new(mapper: mapper)
  end
end

module Shipping
  def event_store
    mapper = RubyEventStore::Mappers::Default.new(
      events_class_remapping: {
        'ordered' => 'Shipping::DeliveryScheduled',
      }
    )
    RailsEventStore::Client.new(mapper: mapper)
  end
end

2nd: decouple domain events schema

But the domain event is not just a name (event type). Event if we decouple from event class we still might have coupling on the event’s schema level.

module Ordering
  class OrderCancelled < Event
    event_type 'cancelled'

    attribute  :order_no, Types::Strict::Integer
    attribute  :reason, Types::Strict::String.optional
  end
end

module Shipping
  class DeliveryRevoked < Event
    event_type 'cancelled'

    attribute  :order_no, Types::Coercible::String
  end
end

Here we have 2 events. In the beginning, they look different. They have different class names, different schema - however they share some attributes. As defined before these events share event type. As a base class, I use here my implementation of base event class, compatible with RailsEventStore::Event but allowing to define attributes using dry-schema and dry-types gems. You could see the implementation of this base class here. These events have a different schema. But the way they are defined allows usage of the Weak Schema technique.

However to be albe to use the weak schema we need to change the serialization format in Rails Event Store. YAML has been a really bad idea ;) Fortunatelly for us it is very simple with Default mapper:

module Ordering
  def event_store
    mapper = RubyEventStore::Mappers::Default.new(
      serializer: JSON,
      events_class_remapping: {
        'ordered' => 'Ordering::OrderCompleted',
      }
    )
    RailsEventStore::Client.new(mapper: mapper)
  end
end

module Shipping
  def event_store
    mapper = RubyEventStore::Mappers::Default.new(
      serializer: JSON,
      events_class_remapping: {
        'ordered' => 'Shipping::DeliveryScheduled',
      }
    )
    RailsEventStore::Client.new(mapper: mapper)
  end
end

BTW do you know that it is just a wrapper for a PipelineMapper and you could build your mapper by composing any transformations you need? But this is a story for a different post.

There are some rules that you need to be aware of to use the weak schema. The rules for mapping are simple. When reading the event from the event store, you look at the serialized JSON and the event instance. And then the rules apply:

  • Exists on JSON and instance -> value from JSON
  • Exists on JSON but not on the instance -> NOP
  • Exists on the instance but not in JSON -> default value

You could read more about Weak Schema in Event Versioning book by Greg Young (available for free to read on LeanPub).

With the use of dry-types attributes, we could also define coercion rules (i.e. replacing integers with strings) and define default values.

3rd: decouple persistence & pub/sub

The last coupling to avoid is the persistence & publishing of the domain events. I’ve already mentioned the solution here. You just don’t publish outside of your bounded context (module) the internal events you use to persist state changes of the aggregates. This technique has several advantages:

  • you define “the contract” between your BCs - read more about Open Host Service & Published Language context relationships
  • changes of the contract could be versioned - i.e. you could publish 2 versions of the same public event until all downstream contexts (clients) will catch up and will be able to handle the latest version
  • the changes in internal domain events schema do not have an impact on published public events
  • you could enrich published events with additional data and publish the result of several internal events as an summary event
  • use event class remapping as a simple form of bounded context anti-corruption layer

The separation could be done via physical separation of data in different data stores. In this solution, each BC should have its private data store and specific Rails Event Store configuration, and an additional Rails Event Store (or any other pub/sub implementation that will support Weak Schema) as a communication interface between different bounded contexts.

In a modular-monolith application, we could simplify this by using only a single instance of Rails Event Store and separate domain events on streams level. This will require more reliance on conventions and discipline of the development team as there is no such restriction implemented in Rails Event Store.

The mixed model is also possible. Separate instances of Rails Event Store with a wrapper for EventRepository to force the convention by adding module prefix to stream names. This way we still have single data store but each context (module) could only write to its streams and “public” streams via the RES instance which is used to communicate between bounded contexts.

You might also like